diff --git a/src/Cli/dotnet/Commands/Workload/Install/WindowsMsiManifestInstaller.cs b/src/Cli/dotnet/Commands/Workload/Install/WindowsMsiManifestInstaller.cs
index a852b715f8ba..48ecd1ec70eb 100644
--- a/src/Cli/dotnet/Commands/Workload/Install/WindowsMsiManifestInstaller.cs
+++ b/src/Cli/dotnet/Commands/Workload/Install/WindowsMsiManifestInstaller.cs
@@ -129,14 +129,7 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath)
}
}
- var manifestsFolder = Path.Combine(msiExtractionPath, "dotnet", "sdk-manifests");
-
- string? manifestFolder = null;
- string? manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault();
- if (manifestsFeatureBandFolder != null)
- {
- manifestFolder = Directory.GetDirectories(manifestsFeatureBandFolder).SingleOrDefault();
- }
+ string? manifestFolder = FindExtractedManifestFolder(msiExtractionPath);
if (manifestFolder == null)
{
@@ -155,6 +148,42 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath)
}
}
+ ///
+ /// Locates the extracted manifest directory in the layout produced by the administrative install of a
+ /// manifest or workload set MSI.
+ ///
+ ///
+ /// The MSI lays its content down under <Program Files>\dotnet\sdk-manifests\<feature band>\...,
+ /// but the name of the Program Files directory in the administrative image depends on the WiX version that
+ /// built the MSI: WiX v3 collapsed it into the target directory (<target>\dotnet\sdk-manifests),
+ /// while WiX v4+ emits a named directory for it (for example <target>\PFiles64\dotnet\sdk-manifests).
+ /// Search for the sdk-manifests directory instead of assuming a fixed depth so that both layouts work.
+ ///
+ ///
+ /// The directory the MSI was administratively installed to.
+ /// The extracted manifest directory, or if it could not be located.
+ internal static string? FindExtractedManifestFolder(string msiExtractionPath)
+ {
+ if (!Directory.Exists(msiExtractionPath))
+ {
+ return null;
+ }
+
+ string? manifestsFolder = Directory.EnumerateDirectories(msiExtractionPath, "sdk-manifests", SearchOption.AllDirectories).FirstOrDefault();
+ if (manifestsFolder == null)
+ {
+ return null;
+ }
+
+ string? manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault();
+ if (manifestsFeatureBandFolder == null)
+ {
+ return null;
+ }
+
+ return Directory.GetDirectories(manifestsFeatureBandFolder).SingleOrDefault();
+ }
+
///
/// Equivalent to .
///
diff --git a/test/dotnet.Tests/CommandTests/Workload/Install/GivenAWindowsMsiManifestInstaller.cs b/test/dotnet.Tests/CommandTests/Workload/Install/GivenAWindowsMsiManifestInstaller.cs
index fd4822a3cd6a..a541731d63e6 100644
--- a/test/dotnet.Tests/CommandTests/Workload/Install/GivenAWindowsMsiManifestInstaller.cs
+++ b/test/dotnet.Tests/CommandTests/Workload/Install/GivenAWindowsMsiManifestInstaller.cs
@@ -39,4 +39,36 @@ public void GetManifestPackageIdReturnsTheArchitectureQualifiedManifestPackageId
packageId.ToString().Should().Be(
$"{manifestId}.Manifest-{featureBand}.Msi.{RuntimeInformation.ProcessArchitecture}".ToLowerInvariant());
}
+
+ // MSIs built with WiX v3 collapse the Program Files directory into the administrative install target.
+ [TestMethod]
+ public void FindExtractedManifestFolderLocatesTheManifestInTheWiXV3AdminInstallLayout()
+ {
+ var testDirectory = TestAssetsManager.CreateTestDirectory().Path;
+ var expected = Path.Combine(testDirectory, "dotnet", "sdk-manifests", "6.0.100", "test.manifest");
+ Directory.CreateDirectory(expected);
+
+ WindowsMsiManifestInstaller.FindExtractedManifestFolder(testDirectory).Should().Be(expected);
+ }
+
+ // MSIs built with WiX v4 and newer emit a named directory for Program Files in the administrative image.
+ [TestMethod]
+ public void FindExtractedManifestFolderLocatesTheManifestInTheWiXV4AdminInstallLayout()
+ {
+ var testDirectory = TestAssetsManager.CreateTestDirectory().Path;
+ var expected = Path.Combine(testDirectory, "PFiles64", "dotnet", "sdk-manifests", "6.0.100", "workloadsets");
+ Directory.CreateDirectory(expected);
+
+ WindowsMsiManifestInstaller.FindExtractedManifestFolder(testDirectory).Should().Be(expected);
+ }
+
+ [TestMethod]
+ public void FindExtractedManifestFolderReturnsNullWhenThereIsNoManifest()
+ {
+ var testDirectory = TestAssetsManager.CreateTestDirectory().Path;
+ Directory.CreateDirectory(Path.Combine(testDirectory, "PFiles64", "dotnet"));
+
+ WindowsMsiManifestInstaller.FindExtractedManifestFolder(testDirectory).Should().BeNull();
+ WindowsMsiManifestInstaller.FindExtractedManifestFolder(Path.Combine(testDirectory, "does-not-exist")).Should().BeNull();
+ }
}