diff --git a/.editorconfig b/.editorconfig
index b2f8c844227..9576b187900 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -22,6 +22,10 @@ tab_width = 2
indent_size = 4
tab_width = 4
+# WiX files
+[*.{wixproj,wxs,wxi,wxl,thm}]
+indent_size = 2
+
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2f550e784ce..66d69fefec8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -81,8 +81,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 35804da0968..3eaae8a1ef4 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/eng/common/native/NativeAotSupported.props b/eng/common/native/NativeAotSupported.props
index 559a6663929..cdff9ef0361 100644
--- a/eng/common/native/NativeAotSupported.props
+++ b/eng/common/native/NativeAotSupported.props
@@ -13,6 +13,8 @@
<_NativeAotSupportedArch Condition="
'$(TargetArchitecture)' != 'wasm' and
+ '$(TargetArchitecture)' != 's390x' and
+ '$(TargetArchitecture)' != 'ppc64le' and
('$(TargetArchitecture)' != 'x86' or '$(TargetOS)' == 'windows')
">true
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs
index ded16c69915..bd5fe0003ba 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs
@@ -8,6 +8,7 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.Build.Tasks.Workloads.Msi;
+using WixToolset.Dtf.WindowsInstaller;
using Xunit;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
@@ -15,70 +16,94 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
public class CreateVisualStudioWorkloadSetTests : TestBase
{
[WindowsOnlyFact]
- public static void ItCanCreateWorkloadSets()
+ public void ItCanCreateWorkloadSets()
{
// Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up
// conflicting sources from previous runs.
- string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLS");
+ string testCaseDirectory = GetTestCaseDirectory();
+ string baseIntermediateOutputPath = testCaseDirectory;
if (Directory.Exists(baseIntermediateOutputPath))
{
Directory.Delete(baseIntermediateOutputPath, recursive: true);
}
- ITaskItem[] workloadSetPackages = new[]
- {
+ ITaskItem[] workloadSetPackages =
+ [
new TaskItem(Path.Combine(TestAssetsPath, "microsoft.net.workloads.9.0.100.9.0.100-baseline.1.23464.1.nupkg"))
.WithMetadata(Metadata.MsiVersion, "12.8.45")
- };
+ ];
- IBuildEngine buildEngine = new MockBuildEngine();
+ var buildEngine = new MockBuildEngine();
CreateVisualStudioWorkloadSet createWorkloadSetTask = new CreateVisualStudioWorkloadSet()
{
- BaseOutputPath = BaseOutputPath,
+ BaseOutputPath = Path.Combine(testCaseDirectory, "msi"),
BaseIntermediateOutputPath = baseIntermediateOutputPath,
BuildEngine = buildEngine,
- WixToolsetPath = WixToolsetPath,
- WorkloadSetPackageFiles = workloadSetPackages
+ WorkloadSetPackageFiles = workloadSetPackages,
+ WixExe = ToolsetInfo.WixExePath,
+ HeatExe = ToolsetInfo.HeatExePath,
+ WixExtensions = WixExtensions,
};
- Assert.True(createWorkloadSetTask.Execute());
+ Assert.True(createWorkloadSetTask.Execute(), buildEngine.BuildErrorEvents.Count > 0 ?
+ buildEngine.BuildErrorEvents[0].Message : "Task failed. No error events");
+
+ // Validate the arm64 installer.
+ ITaskItem arm64Msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "arm64");
+ Assert.NotNull(arm64Msi);
+ ITaskItem x64Msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "x64");
+ Assert.NotNull(x64Msi);
+
+ var arm64MsiPath = arm64Msi.ItemSpec;
+ var x64MsiPath = x64Msi.ItemSpec;
- // Spot check the x64 generated MSI.
- ITaskItem msi = createWorkloadSetTask.Msis.Where(i => i.GetMetadata(Metadata.Platform) == "x64").FirstOrDefault();
- Assert.NotNull(msi);
+ // Process the summary information stream's template to extract the MSIs target platform.
+ using SummaryInfo si = new(arm64MsiPath, enableWrite: false);
+ Assert.Equal("Arm64;1033", si.Template);
- // Verify the workload set records the CLI will use.
- MsiUtils.GetAllRegistryKeys(msi.ItemSpec).Should().Contain(r =>
- r.Root == 2 &&
- r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledWorkloadSets\x64\9.0.100\9.0.100-baseline.1.23464.1" &&
- r.Name == "ProductVersion" &&
- r.Value == "12.8.45");
+ // Upgrades are not supported, but we do generated stable GUIDs based on various
+ // properties including the target platform.
+ string upgradeCode = MsiUtils.GetProperty(arm64MsiPath, MsiProperty.UpgradeCode);
+ Assert.Equal("{A05B88DE-F40F-3C20-B6DA-719B8EED1D9F}", upgradeCode);
+ // Make sure the x64 and arm64 MSIs have different UpgradeCode properties.
+ string x64UpgradeCode = MsiUtils.GetProperty(x64MsiPath, MsiProperty.UpgradeCode);
+ Assert.NotEqual(upgradeCode, x64UpgradeCode);
- // Workload sets are SxS. Verify that we don't have an Upgrade table.
- Assert.False(MsiUtils.HasTable(msi.ItemSpec, "Upgrade"));
+ // Verify the installation record and dependency provider registry entries.
+ var registryKeys = MsiUtils.GetAllRegistryKeys(arm64MsiPath);
+ string productCode = MsiUtils.GetProperty(arm64MsiPath, MsiProperty.ProductCode);
+ string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledWorkloadSets\arm64\9.0.100\9.0.100-baseline.1.23464.1";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Set,9.0.100,9.0.100-baseline.1.23464.1,arm64";
+
+ // ProductCode and UpgradeCode values in the installation record should match the
+ // values from the Property table.
+ ValidateInstallationRecord(registryKeys, installationRecordKey,
+ "Microsoft.NET.Workload.Set,9.0.100,9.0.100-baseline.1.23464.1,arm64",
+ productCode, upgradeCode, "12.8.45");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
// Verify the workloadset version directory and only look at the long name version.
- DirectoryRow versionDir = MsiUtils.GetAllDirectories(msi.ItemSpec).FirstOrDefault(d => string.Equals(d.Directory, "WorkloadSetVersionDir"));
+ DirectoryRow versionDir = MsiUtils.GetAllDirectories(arm64MsiPath).FirstOrDefault(d => string.Equals(d.Directory, "WorkloadSetVersionDir"));
Assert.NotNull(versionDir);
Assert.Contains("|9.0.0.100-baseline.1.23464.1", versionDir.DefaultDir);
- // Verify the SWIX authoring for one of the workload set MSIs.
- ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\x64")).FirstOrDefault();
- Assert.Equal(DefaultValues.PackageTypeMsiWorkloadSet, workloadSetSwixItem.GetMetadata(Metadata.PackageType));
+ // Verify that the workloadset.json exists.
+ var files = MsiUtils.GetAllFiles(arm64MsiPath);
+ files.Should().Contain(f => f.FileName.EndsWith("|workloadset.json"));
+ // Verify the SWIX authoring for one of the workload set MSIs.
+ ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.FirstOrDefault(
+ s => s.GetMetadata(Metadata.PackageType) == DefaultValues.PackageTypeMsiWorkloadSet);
string msiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(workloadSetSwixItem.ItemSpec), "msi.swr"));
Assert.Contains("package name=Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1", msiSwr);
Assert.Contains("version=12.8.45", msiSwr);
- Assert.DoesNotContain("vs.package.chip=x64", msiSwr);
- Assert.Contains("vs.package.machineArch=x64", msiSwr);
Assert.Contains("vs.package.type=msi", msiSwr);
// Verify package group SWIX project
- ITaskItem workloadSetPackageGroupSwixItem = createWorkloadSetTask.SwixProjects.Where(
- s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeWorkloadSetPackageGroup)).
- FirstOrDefault();
+ ITaskItem workloadSetPackageGroupSwixItem = createWorkloadSetTask.SwixProjects.FirstOrDefault(
+ s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeWorkloadSetPackageGroup));
string packageGroupSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(workloadSetPackageGroupSwixItem.ItemSpec), "packageGroup.swr"));
Assert.Contains("package name=PackageGroup.NET.Workloads-9.0.100", packageGroupSwr);
Assert.Contains("vs.dependency id=Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1", packageGroupSwr);
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs
index e95fd0db6b2..482ecef1631 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs
@@ -2,15 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
+using AwesomeAssertions;
using Microsoft.Arcade.Test.Common;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-using Microsoft.Deployment.WindowsInstaller;
using Microsoft.DotNet.Build.Tasks.Workloads.Msi;
+using WixToolset.Dtf.WindowsInstaller;
using Xunit;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
@@ -18,54 +19,115 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
[Collection("Workload Creation")]
public class CreateVisualStudioWorkloadTests : TestBase
{
+ [SkipOnCI(reason: "This test builds the full WASM workload.")]
[WindowsOnlyFact]
- public static void ItCanCreateWorkloads()
+ public void ItCreatesPackGroups()
{
+ string packageSource = Path.Combine(TestAssetsPath, "wasm");
// Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up
// conflicting sources from previous runs.
- string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WL");
+ string testCaseDirectory = GetTestCaseDirectory();
+ string baseIntermediateOutputPath = testCaseDirectory;
- if (Directory.Exists(baseIntermediateOutputPath))
+ ITaskItem[] manifestsPackages =
{
- Directory.Delete(baseIntermediateOutputPath, recursive: true);
- }
+ new TaskItem(Path.Combine(packageSource, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg"))
+ .WithMetadata(Metadata.MsiVersion, "10.0.456")
+ };
- ITaskItem[] manifestsPackages = new[]
+ IBuildEngine buildEngine = new MockBuildEngine();
+ CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload()
{
- new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg"))
- .WithMetadata(Metadata.MsiVersion, "6.33.28")
+ AllowMissingPacks = true,
+ BaseOutputPath = Path.Combine(testCaseDirectory, "bin"),
+ BaseIntermediateOutputPath = baseIntermediateOutputPath,
+ BuildEngine = buildEngine,
+ ComponentResources = Array.Empty(),
+ CreateWorkloadPackGroups = true,
+ DisableParallelPackageGroupProcessing = false,
+ IsOutOfSupportInVisualStudio = false,
+ ManifestMsiVersion = null,
+ PackageSource = packageSource,
+ ShortNames = Array.Empty(),
+ WixExe = ToolsetInfo.WixExePath,
+ HeatExe = ToolsetInfo.HeatExePath,
+ WixExtensions = WixExtensions,
+ WorkloadManifestPackageFiles = manifestsPackages
};
- ITaskItem[] componentResources = new[]
- {
+ bool result = createWorkloadTask.Execute();
+ Assert.True(result);
+
+ // Verify that the Visual Studio workload components reference workload pack groups.
+ string componentSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.ItemSpec.Contains("wasm.tools.10.0.swixproj")).ItemSpec), "component.swr"));
+ Assert.Contains("vs.dependency id=wasm.tools.WorkloadPacks", componentSwr);
+
+ // Manifest installers should contain additional JSON files describing pack groups.
+ ITaskItem manifestMsi = createWorkloadTask.Msis.First(m => m.GetMetadata(Metadata.PackageType) == "manifest");
+ MsiUtils.GetAllFiles(manifestMsi.ItemSpec).Should().Contain(f => f.FileName.EndsWith("WorkloadPackGroups.json"));
+
+ // Verify the package group JSON and ensure there are no duplicates.
+ var json = File.ReadAllText(Path.Combine(manifestMsi.GetMetadata(Metadata.SourcePath), "json", "WorkloadPackGroups.json"));
+ var groupIds = JsonSerializer.Deserialize(json).EnumerateArray();
+ groupIds.Count().Should().Be(groupIds.Distinct().Count(), "because there should be no duplicate pack group IDs");
+
+ // Verify that the workload component contains a reference to a workload pack group package.
+ string iosComponentSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.ItemSpec.Contains("microsoft.net.runtime.ios.10.0.swixproj")).ItemSpec), "component.swr"));
+ iosComponentSwr.Should().Contain("vs.dependency id=microsoft.net.runtime.ios.WorkloadPacks");
+ }
+
+ [WindowsOnlyFact]
+ public void ItCanCreateWorkloads()
+ {
+ // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up
+ // conflicting sources from previous runs.
+ string testCaseDirectory = GetTestCaseDirectory();
+ string baseIntermediateOutputPath = testCaseDirectory;
+
+ ITaskItem[] manifestsPackages =
+ [
+ new TaskItem(Path.Combine(TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg"))
+ .WithMetadata(Metadata.MsiVersion, "6.33.28")
+ ];
+
+ ITaskItem[] componentResources =
+ [
new TaskItem("microsoft-net-sdk-emscripten")
.WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)")
.WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.")
.WithMetadata(Metadata.Version, "5.6.7.8")
- };
+ ];
- ITaskItem[] shortNames = new[]
- {
+ ITaskItem[] shortNames =
+ [
new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"),
new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft")
- };
+ ];
IBuildEngine buildEngine = new MockBuildEngine();
CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload()
{
AllowMissingPacks = true,
- BaseOutputPath = TestBase.BaseOutputPath,
+ BaseOutputPath = Path.Combine(testCaseDirectory, "bin"),
BaseIntermediateOutputPath = baseIntermediateOutputPath,
BuildEngine = buildEngine,
ComponentResources = componentResources,
ManifestMsiVersion = null,
- PackageSource = TestBase.TestAssetsPath,
+ PackageSource = TestAssetsPath,
ShortNames = shortNames,
- WixToolsetPath = TestBase.WixToolsetPath,
+ WixExe = ToolsetInfo.WixExePath,
+ HeatExe = ToolsetInfo.HeatExePath,
+ WixExtensions = WixExtensions,
WorkloadManifestPackageFiles = manifestsPackages,
IsOutOfSupportInVisualStudio = true
};
@@ -125,28 +187,36 @@ public static void ItCanCreateWorkloads()
Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", previewComponentSwr);
// Verify the SWIX authoring for the VS package wrapping the manifest MSI
- string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "x64", "msi.swr"));
+ string manifestMsiSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.GetMetadata(Metadata.PackageType) == DefaultValues.PackageTypeMsiManifest).ItemSpec), "msi.swr"));
Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr);
Assert.Contains("vs.package.type=msi", manifestMsiSwr);
- Assert.Contains("vs.package.chip=x64", manifestMsiSwr);
+ Assert.Contains("vs.package.chip=", manifestMsiSwr);
Assert.DoesNotContain("vs.package.machineArch", manifestMsiSwr);
Assert.DoesNotContain("vs.package.outOfSupport", manifestMsiSwr);
- // Verify that no arm64 MSI authoring for VS. EMSDK doesn't define RIDs for arm64, but manifests always generate
- // arm64 MSIs for the CLI based installs so we should not see that.
- string swixRootDirectory = Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200");
- IEnumerable arm64Directories = Directory.EnumerateDirectories(swixRootDirectory, "arm64", SearchOption.AllDirectories);
- Assert.DoesNotContain(arm64Directories, s => s.Contains("arm64"));
+ // There should be no SWIX projects generated targeting arm64 when VS does not support it.
+ createWorkloadTask.SwixProjects.Where(s => s.GetMetadata(Metadata.Platform) == "arm64").Should().BeEmpty();
// Verify the SWIX authoring for one of the workload pack MSIs. Packs get assigned random sub-folders so we
// need to filter out the SWIX project output items the task produced.
- ITaskItem pythonPackSwixItem = createWorkloadTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.Emscripten.Python.6.0.4\x64")).FirstOrDefault();
+ ITaskItem pythonPackSwixItem = createWorkloadTask.SwixProjects.FirstOrDefault(s =>
+ s.GetMetadata(Metadata.PackageType) == DefaultValues.PackageTypeMsiPack &&
+ s.GetMetadata(Metadata.Platform) == "x64" &&
+ s.GetMetadata(Metadata.SwixPackageId).Contains("Python"));
string packMsiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(pythonPackSwixItem.ItemSpec), "msi.swr"));
Assert.Contains("package name=Microsoft.Emscripten.Python.6.0.4", packMsiSwr);
Assert.Contains("vs.package.chip=x64", packMsiSwr);
Assert.Contains("vs.package.outOfSupport=yes", packMsiSwr);
Assert.DoesNotContain("vs.package.machineArch", packMsiSwr);
+ string m = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.GetMetadata(Metadata.PackageType) == DefaultValues.PackageTypeMsiManifest).ItemSpec), "msi.swr"));
+
// Verify the swix project items for components. The project files names always contain the major.minor suffix, so we'll end up
// with microsoft.net.sdk.emscripten.5.6.swixproj and microsoft.net.sdk.emscripten.pre.5.6.swixproj
IEnumerable swixComponentProjects = createWorkloadTask.SwixProjects.Where(s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeComponent));
@@ -155,54 +225,52 @@ public static void ItCanCreateWorkloads()
}
[WindowsOnlyFact]
- public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio()
+ public void ItCanCreateWorkloadsThatSupportArm64InVisualStudio()
{
// Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up
// conflicting sources from previous runs.
- string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLa64");
-
- if (Directory.Exists(baseIntermediateOutputPath))
- {
- Directory.Delete(baseIntermediateOutputPath, recursive: true);
- }
+ string testCaseDirectory = GetTestCaseDirectory();
+ string baseIntermediateOutputPath = testCaseDirectory;
- ITaskItem[] manifestsPackages = new[]
- {
+ ITaskItem[] manifestsPackages =
+ [
new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg"))
.WithMetadata(Metadata.MsiVersion, "6.33.28")
.WithMetadata(Metadata.SupportsMachineArch, "true")
- };
+ ];
- ITaskItem[] componentResources = new[]
- {
+ ITaskItem[] componentResources =
+ [
new TaskItem("microsoft-net-sdk-emscripten")
.WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)")
.WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.")
.WithMetadata(Metadata.Version, "5.6.7.8")
- };
+ ];
- ITaskItem[] shortNames = new[]
- {
+ ITaskItem[] shortNames =
+ [
new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"),
new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"),
new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft")
- };
+ ];
IBuildEngine buildEngine = new MockBuildEngine();
CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload()
{
AllowMissingPacks = true,
- BaseOutputPath = TestBase.BaseOutputPath,
+ BaseOutputPath = Path.Combine(testCaseDirectory, "bin"),
BaseIntermediateOutputPath = baseIntermediateOutputPath,
BuildEngine = buildEngine,
ComponentResources = componentResources,
ManifestMsiVersion = null,
- PackageSource = TestBase.TestAssetsPath,
+ PackageSource = TestAssetsPath,
ShortNames = shortNames,
- WixToolsetPath = TestBase.WixToolsetPath,
+ WixExe = ToolsetInfo.WixExePath,
+ HeatExe = ToolsetInfo.HeatExePath,
+ WixExtensions = WixExtensions,
WorkloadManifestPackageFiles = manifestsPackages,
};
@@ -247,11 +315,14 @@ public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio()
Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr);
// Verify the SWIX authoring for the VS package wrapping the manifest MSI
- string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "arm64", "msi.swr"));
+ string manifestMsiSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.GetMetadata(Metadata.PackageType) == DefaultValues.PackageTypeMsiManifest).ItemSpec), "msi.swr"));
Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr);
Assert.Contains("vs.package.type=msi", manifestMsiSwr);
Assert.DoesNotContain("vs.package.chip", manifestMsiSwr);
- Assert.Contains("vs.package.machineArch=arm64", manifestMsiSwr);
+ Assert.Contains("vs.package.machineArch=", manifestMsiSwr);
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj
index ddbfc1f02f7..78366868e0d 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj
@@ -12,29 +12,88 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
@@ -47,6 +106,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -56,12 +150,22 @@
-
-
-
-
-
-
-
+
+
+ $([System.IO.File]::ReadAllText(ToolsetInfo.cs.pp))
+ $(ToolsetInfoText.Replace('{WixExePath}', $(PkgMicrosoft_Wix)\tools\net6.0\any\wix.exe))
+
+ $(ToolsetInfoText.Replace('{HeatExePath}', $(PkgMicrosoft_WixToolset_Heat)\tools\net472\x64\heat.exe))
+
+ $(ToolsetInfoText.Replace('{DependencyExt}', $(PkgMicrosoft_WixToolset_Dependency_wixext)\wixext6\WixToolset.Dependency.wixext.dll))
+ $(ToolsetInfoText.Replace('{UIExt}', $(PkgMicrosoft_WixToolset_UI_wixext)\wixext6\WixToolset.UI.wixext.dll))
+ $(ToolsetInfoText.Replace('{UtilExt}', $(PkgMicrosoft_WixToolset_Util_wixext)\wixext6\WixToolset.Util.wixext.dll))
+ $(ToolsetInfoText.Replace('{SwixTargetsPath}', $(PkgMicrosoft_VisualStudioEng_MicroBuild_Plugins_SwixBuild)\build\Microsoft.VisualStudioEng.MicroBuild.Plugins.SwixBuild.targets))
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTestFixture.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTestFixture.cs
new file mode 100644
index 00000000000..c6fcebb38c7
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTestFixture.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
+{
+ internal class MsiTestFixture : IDisposable
+ {
+ bool _retainOutput;
+
+ ///
+ /// File system path of the root directory where test output is generated.
+ ///
+ public string OutputPath
+ {
+ get;
+ init;
+ }
+
+ ///
+ /// Files system path where MSIs are generated.
+ ///
+ public string MsiPath
+ {
+ get;
+ init;
+ }
+
+ ///
+ /// File system path where NuGet packages used by the test will be extracted
+ ///
+ public string PackagePath
+ {
+ get;
+ init;
+ }
+
+ public MsiTestFixture(bool retainOutput = false)
+ {
+ OutputPath = Path.Combine(AppContext.BaseDirectory,
+ "TEST_OUTPUT", Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+ Directory.CreateDirectory(OutputPath);
+
+ MsiPath = Path.Combine(OutputPath, "msi");
+ PackagePath = Path.Combine(OutputPath, "pkg");
+
+ _retainOutput = retainOutput;
+ }
+
+ public void Dispose()
+ {
+ if (!_retainOutput)
+ {
+ try
+ {
+ if (Directory.Exists(OutputPath))
+ {
+ // Best effort to clean up output.
+ Directory.Delete(OutputPath, recursive: true);
+ }
+ }
+ catch { }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
index 6d8db4df90b..e7c77478e08 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
@@ -2,54 +2,62 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using AwesomeAssertions;
using Microsoft.Arcade.Test.Common;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-using Microsoft.Deployment.WindowsInstaller;
+using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Build.Tasks.Workloads.Msi;
+using Microsoft.DotNet.Build.Tasks.Workloads.Swix;
using Microsoft.NET.Sdk.WorkloadManifestReader;
+using WixToolset.Dtf.WindowsInstaller;
using Xunit;
+using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
{
[Collection("6.0.200 Toolchain manifest tests")]
public class MsiTests : TestBase
{
- private static ITaskItem BuildManifestMsi(string path, string msiVersion = "1.2.3", string platform = "x64", string msiOutputPath = null)
+ ///
+ /// Helper method to build a manifest MSI from a given manifest package.
+ ///
+ /// The file system path of the output directory used for creating the WiX source and MSI.
+ /// The file system path of the NuGet package containing the workload manifest.
+ /// The version of the MSI to create.
+ /// The platform for the MSI.
+ /// Whether MSIs should allow side-by-side installations instead of major upgrades.
+ /// A task item with metadata for the generated MSI.
+ private static ITaskItem BuildManifestMsi(string outputPath, string manifestPackagePath, string msiVersion = "1.2.3", string platform = "x64",
+ bool allowSideBySideInstalls = true)
{
- string packageRootDirectory = Path.Combine(PackageRootDirectory, Guid.NewGuid().ToString("N"));
- TaskItem packageItem = new(path);
- WorkloadManifestPackage pkg = new(packageItem, packageRootDirectory, new Version(msiVersion));
+ Directory.CreateDirectory(outputPath);
+ TaskItem packageItem = new(manifestPackagePath);
+ WorkloadManifestPackage pkg = new(packageItem, Path.Combine(outputPath, "pkg"),
+ new Version(msiVersion));
pkg.Extract();
- WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath,
- isSxS: true);
- return string.IsNullOrWhiteSpace(msiOutputPath) ? msi.Build(MsiOutputPath) : msi.Build(msiOutputPath);
- }
-
- [WindowsOnlyFact]
- public void WorkloadManifestsIncludeInstallationRecords()
- {
- ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"),
- msiOutputPath: Path.Combine(MsiOutputPath, "mrec"));
- string msiPath603 = msi603.GetMetadata(Metadata.FullPath);
+ WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetConfig,
+ outputPath, isSxS: allowSideBySideInstalls);
- MsiUtils.GetAllRegistryKeys(msiPath603).Should().Contain(r =>
- r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"
- );
+ return msi.Build(Path.Combine(outputPath, "msi"));
}
[WindowsOnlyFact]
public void ItCanBuildSideBySideManifestMsis()
{
+ using var fixture = new MsiTestFixture();
+
// Build 6.0.200 manifest for version 6.0.3
- ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"));
+ ITaskItem msi603 = BuildManifestMsi(fixture.OutputPath, Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"));
string msiPath603 = msi603.GetMetadata(Metadata.FullPath);
// Build 6.0.200 manifest for version 6.0.4
- ITaskItem msi604 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.4.nupkg"));
+ ITaskItem msi604 = BuildManifestMsi(fixture.OutputPath, Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.4.nupkg"));
string msiPath604 = msi604.GetMetadata(Metadata.FullPath);
// For upgradable MSIs, the 6.0.4 and 6.0.3 copies of the package would have generated the same
@@ -69,76 +77,305 @@ public void ItCanBuildSideBySideManifestMsis()
d.Directory == "ManifestVersionDir" &&
d.DirectoryParent == "ManifestIdDir" &&
d.DefaultDir.EndsWith("|6.0.4"));
+ }
+
+ [WindowsOnlyTheory]
+ [InlineData(true, null, "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,6.0.3,x64")]
+ [InlineData(true, null, "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,6.0.3,x86", "x86")]
+ [InlineData(false, "{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64")]
+ [InlineData(false, "{239D1181-C3CE-3E9E-91FE-3A645B0077B2}", "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,arm64", "arm64")]
+ public void ItCanBuildAManifestMsi(bool allowSideBySideInstalls, string expectedUpgradeCode,
+ string expectedProviderKeyName, string platform = "x64")
+ {
+ using var fixture = new MsiTestFixture(true);
+
+ ITaskItem msi = BuildManifestMsi(fixture.OutputPath,
+ Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"),
+ platform: platform,
+ allowSideBySideInstalls: allowSideBySideInstalls);
+
+ msi.GetMetadata(Metadata.PackageType).Should().Be("manifest", "because we're building a manifest MSI");
+
+ string msiPath = msi.GetMetadata(Metadata.FullPath);
+
+ string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode);
+
+ if (!allowSideBySideInstalls)
+ {
+ // UpgradeCode is predictable/stable for manifest MSIs that support major upgrades.
+ Assert.Equal(expectedUpgradeCode, upgradeCode);
+
+ // Upgrade table should contain two rows, one of which is only used to detect downgrades.
+ var relatedProducts = MsiUtils.GetRelatedProducts(msiPath);
+ ValidatedRelatedProduct(relatedProducts, $"{expectedUpgradeCode:B}", null, "1.2.3", 1, "WIX_UPGRADE_DETECTED");
+ ValidatedRelatedProduct(relatedProducts, $"{expectedUpgradeCode:B}", "1.2.3", null, 2, "WIX_DOWNGRADE_DETECTED");
+
+ // There should be no version directory present if the old upgrade model is used.
+ MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir",
+ "because the manifest MSI supports major upgrades");
+ }
+ else
+ {
+ // We can technically remove the Upgrade table by setting Package@UpgradeStrategy="none", but even
+ // for SxS installs we still write some information to the JSON manifest that the CLI uses and the
+ // absence of the table might have unforseen consequences with VS authoring and SWIX toolset.
+
+ // The versioned manifest directory is required to support SxS installs.
+ MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("ManifestVersionDir",
+ "because the manifest MSI supports major upgrades");
+ }
+
+ Assert.Equal("1.2.3", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion));
+ // The same ProviderKey is used across different versions when upgrades are supported,
+ // but for SxS installs, the package version is included to differentiate it.
+ Assert.Equal(expectedProviderKeyName, MsiUtils.GetProviderKeyName(msiPath));
+
+ // Process the summary information stream's template to extract the MSIs target platform.
+ ValidateSummaryInformation(msiPath, platform);
+
+ // Verify the installation record and dependency provider registry entries
+ var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath);
+ string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode);
+ string installationRecordKey = $@"SOFTWARE\Microsoft\dotnet\InstalledManifests\{platform}\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\" + expectedProviderKeyName;
+
+ ValidateInstallationRecord(registryKeys, installationRecordKey,
+ expectedProviderKeyName,
+ expectedProductCode, upgradeCode, "1.2.3");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
- // Generated MSI should return the path where the .wixobj files are located so
- // WiX packs can be created for post-build signing.
- Assert.NotNull(msi603.GetMetadata(Metadata.WixObj));
- Assert.NotNull(msi604.GetMetadata(Metadata.WixObj));
+ var customActions = MsiUtils.GetCustomActions(msiPath);
+ ValidateDotNetHomeCustomActions(customActions, platform);
+
+ // The File table should contain the workload manifest and targets. There may be additional
+ // localized content for the manifests. Their presence is neither required nor critical to
+ // how workloads functions.
+ var files = MsiUtils.GetAllFiles(msiPath);
+ files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.json"));
+ files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.targets"));
}
[WindowsOnlyFact]
- public void ItCanBuildAManifestMsi()
+ public void ItCanBuildWorkloadSdkPackMsi()
{
- string packageRootDirectory = Path.Combine(PackageRootDirectory, Guid.NewGuid().ToString("N"));
- TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"));
- WorkloadManifestPackage pkg = new(packageItem, packageRootDirectory, new Version("1.2.3"));
- pkg.Extract();
- WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath);
+ using var fixture = new MsiTestFixture(true);
+
+ TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg"));
+ WorkloadManifestPackage manifestPackage = new(packageItem, fixture.PackagePath, new Version("1.2.3"));
+ // Parse the manifest to extract information related to workload packs so we can extract a specific pack.
+ WorkloadManifest manifest = manifestPackage.GetManifest();
+ WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Python");
+ WorkloadPack pack = manifest.Packs[packId];
+
+ var sourcePackages = WorkloadPackPackage.GetSourcePackages(TestAssetsPath, pack);
+ var sourcePackageInfo = sourcePackages.FirstOrDefault();
+ var workloadPackPackage = WorkloadPackPackage.Create(pack, sourcePackageInfo.sourcePackage, sourcePackageInfo.platforms, fixture.PackagePath, null, null);
+ workloadPackPackage.Extract();
- ITaskItem item = msi.Build(MsiOutputPath);
+ var workloadPackMsi = new WorkloadPackMsi(workloadPackPackage, "x64", new MockBuildEngine(),
+ WixToolsetConfig, fixture.OutputPath);
- string msiPath = item.GetMetadata(Metadata.FullPath);
+ // Build the MSI and verify its contents
+ var msiItem = workloadPackMsi.Build(fixture.MsiPath);
+ string msiPath = msiItem.GetMetadata(Metadata.FullPath);
// Process the summary information stream's template to extract the MSIs target platform.
using SummaryInfo si = new(msiPath, enableWrite: false);
-
- // UpgradeCode is predictable/stable for manifest MSIs.
- Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode));
- Assert.Equal("1.2.3", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion));
- Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", MsiUtils.GetProviderKeyName(msiPath));
Assert.Equal("x64;1033", si.Template);
- // There should be no version directory present if the old upgrade model is used.
- MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir");
+ // Verify pack directories
+ var directories = MsiUtils.GetAllDirectories(msiPath);
+ directories.Select(d => d.Directory).Should().Contain("PackageDir", "because it's an SDK pack");
+ directories.Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack");
+
+ // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version).
+ string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode);
+ Assert.Equal("{BDE8712D-9BD7-3692-9C2A-C518208967D6}", upgradeCode);
+
+ // Verify the installation record and dependency provider registry entries
+ var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath);
+ string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode);
+ string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64\6.0.4";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64";
- // Generated MSI should return the path where the .wixobj files are located so
- // WiX packs can be created for post-build signing.
- Assert.NotNull(item.GetMetadata(Metadata.WixObj));
+ ValidateInstallationRecord(registryKeys, installationRecordKey,
+ "Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64",
+ expectedProductCode, upgradeCode, "6.0.4.0");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
}
[WindowsOnlyFact]
public void ItCanBuildATemplatePackMsi()
{
- string packageRootDirectory = Path.Combine(PackageRootDirectory, Guid.NewGuid().ToString("N"));
+ if (string.IsNullOrEmpty(MSBuildExePath))
+ return; // machine doesn't have native toolchain
+
+ using var fixture = new MsiTestFixture(true);
+
string packagePath = Path.Combine(TestAssetsPath, "microsoft.ios.templates.15.2.302-preview.14.122.nupkg");
WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null);
- TemplatePackPackage pkg = new(p, packagePath, new[] { "x64" }, packageRootDirectory);
+ TemplatePackPackage pkg = new(p, packagePath, ["x64"], fixture.PackagePath);
pkg.Extract();
var buildEngine = new MockBuildEngine();
- WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetPath, BaseIntermediateOutputPath);
- ITaskItem item = msi.Build(MsiOutputPath);
+ WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetConfig, fixture.OutputPath, createWixPack: true);
+ ITaskItem msiItem = msi.Build(fixture.MsiPath);
+
+ msiItem.GetMetadata(Metadata.PackageType).Should().Be("pack", "because we're building a template pack MSI");
+
+ // Just do basic sanity checks. Validating the contents of the wixpack is outside the scope of this test.
+ string wixPack = msiItem.GetMetadata(Metadata.WixPack);
+ wixPack.Should().NotBeEmpty("because we're generating a wixpack for signing");
+ File.Exists(wixPack).Should().BeTrue("because the wixpack path should point to an existing file");
- string msiPath = item.GetMetadata(Metadata.FullPath);
+ string msiPath = msiItem.GetMetadata(Metadata.FullPath);
// Process the summary information stream's template to extract the MSIs target platform.
using SummaryInfo si = new(msiPath, enableWrite: false);
// UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version).
- Assert.Equal("{EC4D6B34-C9DE-3984-97FD-B7AC96FA536A}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode));
+ string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode);
+ Assert.Equal("{EC4D6B34-C9DE-3984-97FD-B7AC96FA536A}", upgradeCode);
// The version is set using the package major.minor.patch
Assert.Equal("15.2.302.0", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion));
Assert.Equal("Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", MsiUtils.GetProviderKeyName(msiPath));
Assert.Equal("x64;1033", si.Template);
- // Template packs should pull in the raw nupkg. We can verify by query the File table. There should
+ // Template packs should pull in the raw nupkg. We can verify this by querying the File table. There should
// only be a single file.
FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault();
- Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupk", fileRow.FileName);
+ Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupkg", fileRow.FileName);
+
+ // Verify that the generated component GUID for the template pack is stable. This value
+ // should only change if component's keypath changes. The check also checks the foreign key reference
+ // from the file row into the component table.
+ MsiUtils.GetAllComponents(msiPath).Should().Contain(c =>
+ c.ComponentId == "{98827ECA-69A2-5300-A75E-F1A251EB17F9}" &&
+ c.Component == fileRow.Component_);
+
+ // Verify the installation record and dependency provider registry entries
+ var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath);
+ string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode);
+ string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.iOS.Templates\15.2.302-preview.14.122";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.iOS.Templates,15.2.302-preview.14.122,x64";
+
+ ValidateInstallationRecord(registryKeys, installationRecordKey,
+ "Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", expectedProductCode, upgradeCode, "15.2.302.0");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
+
+ // Generate a SWIX project and build it.
+ MsiSwixProject swixProject = new(msiItem, fixture.OutputPath, fixture.OutputPath,
+ ReleaseVersion.Parse("10.0.100"), chip: null, machineArch: msiItem.GetMetadata(Metadata.Platform));
+
+ string swixProj = swixProject.Create();
+ // Output path for the SWIX JSON manifest
+ string swixManifestOutputPath = Path.Combine(fixture.OutputPath, "swix");
+ string swixManifestPath = Path.Combine(swixManifestOutputPath, Path.GetFileNameWithoutExtension(swixProj) + ".json");
+
+ BuildSwixProject(swixProj, swixManifestOutputPath);
+
+ var options = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.AllowReadingFromString
+ };
+
+ var json = File.ReadAllText(swixManifestPath);
+ var doc = JsonSerializer.Deserialize(json, options);
+
+ var package = doc.GetProperty("packages")
+ .EnumerateArray()
+ .First();
+
+ // WiX 4 introduced a breaking change where the provider key table was renamed. Older versions of
+ // SWIX would fail to read the provider key from the MSI and the property would be missing.
+ package.TryGetProperty("providerKey", out JsonElement providerKey)
+ .Should().BeTrue("the package should contain a 'providerKey' property");
+ providerKey.GetString()
+ .Should().Be("Microsoft.iOS.Templates,15.2.302-preview.14.122,x64");
+ }
+
+ [WindowsOnlyFact]
+ public void ItCanBuildAWorkPackGroupMsi()
+ {
+ using var fixture = new MsiTestFixture(true);
+
+ string packageSource = Path.Combine(TestAssetsPath, "wasm");
+
+ TaskItem packageItem = new(Path.Combine(packageSource, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg"));
+ WorkloadManifestPackage manifestPackage = new(packageItem, fixture.PackagePath, new Version("1.2.3"));
+ // Parse the manifest to extract information related to workload packs so we can extract a specific
+ // workload.
+ WorkloadManifest manifest = manifestPackage.GetManifest();
+ WorkloadId workloadId = new("wasm-tools");
+ WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads[workloadId];
+
+ string packGroupId = null;
+ WorkloadPackGroupJson packGroupJson = null;
+
+ packGroupId = WorkloadPackGroupPackage.GetPackGroupId(workload.Id);
+ packGroupJson = new WorkloadPackGroupJson()
+ {
+ GroupPackageId = packGroupId,
+ GroupPackageVersion = manifestPackage.PackageVersion.ToString()
+ };
+
+ List workloadPackPackages = [];
+
+ foreach (WorkloadPackId packId in workload.Packs)
+ {
+ WorkloadPack pack = manifest.Packs[packId];
+
+ packGroupJson.Packs.Add(new WorkloadPackJson()
+ {
+ PackId = pack.Id,
+ PackVersion = pack.Version
+ });
+
+ string sourcePackage = WorkloadPackPackage.GetSourcePackage(packageSource, pack, "x64");
+
+ if (!string.IsNullOrWhiteSpace(sourcePackage))
+ {
+ workloadPackPackages.Add(WorkloadPackPackage.Create(pack, sourcePackage, ["x64"],
+ fixture.PackagePath, null, null));
+ }
+ }
+
+ var groupPackage = new WorkloadPackGroupPackage(workload.Id, manifestPackage);
+ groupPackage.Packs.AddRange(workloadPackPackages);
+ groupPackage.ManifestsPerPlatform["x64"] = new([manifestPackage]);
+
+ var buildEngine = new MockBuildEngine();
+
+ workloadPackPackages.ForEach(p => p.Extract());
+
+ WorkloadPackGroupMsi msi = new(groupPackage, "x64", buildEngine, WixToolsetConfig, fixture.OutputPath);
+
+ ITaskItem msiItem = msi.Build(fixture.MsiPath);
+
+ msiItem.GetMetadata(Metadata.PackageType).Should().Be("pack-group", "because we're building a workload pack group MSI");
+
+ string msiPath = msiItem.GetMetadata(Metadata.FullPath);
+
+ // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version).
+ string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode);
+ Assert.Equal("{31FEB1C7-842A-33EE-8267-2E46C6DCF3D4}", upgradeCode);
+
+ // Verify the installation record and dependency provider registry entries
+ var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath);
+ string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode);
+ string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\wasm.tools.WorkloadPacks,10.0.100,x64";
+
+ ValidateInstallationRecord(registryKeys, installationRecordKey,
+ "wasm.tools.WorkloadPacks,10.0.100,x64", expectedProductCode, upgradeCode, "1.2.3");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
- // Generated MSI should return the path where the .wixobj files are located so
- // WiX packs can be created for post-build signing.
- Assert.NotNull(item.GetMetadata(Metadata.WixObj));
+ // Pack groups carry additional keys for each pack in the group.
+ ValidatePackGroupInstallRecordKeys(registryKeys,
+ @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Runtime.WebAssembly.Sdk\10.0.0",
+ @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Sdk.WebAssembly.Pack\10.0.0",
+ @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.Mono.browser-wasm\10.0.0",
+ @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.AOT.win-x64.Cross.browser-wasm\10.0.0");
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs
index e589a2cedc3..acda51d2baf 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs
@@ -25,7 +25,7 @@ public void ItAssignsDefaultValues()
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -54,7 +54,7 @@ public void ItCanAdvertiseComponents()
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentResources);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -83,7 +83,7 @@ public void ItPrefersComponentResourcesOverDefaults()
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentResources);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -109,7 +109,7 @@ public void ItShortensComponentIds()
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, shortNames: shortNames);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -123,7 +123,7 @@ public void ItIgnoresNonApplicableDepedencies()
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, null, null);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -152,7 +152,7 @@ public void ItCanOverrideDefaultValues()
};
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, componentResources);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr"));
@@ -168,7 +168,7 @@ public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks()
WorkloadManifest manifest = Create("mauiWorkloadManifest.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest, packGroupId: null);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
@@ -183,7 +183,7 @@ public void ItCreatesDependenciesForPackGroup()
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
var packGroupId = "microsoft.net.sdk.blazorwebassembly.aot.WorkloadPacks";
SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest, packGroupId: packGroupId);
- ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
+ ComponentSwixProject project = new(component, GetTestCaseDirectory(), BaseOutputPath);
string swixProj = project.Create();
string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
index 99b98e1b112..053848796ec 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
@@ -27,7 +27,7 @@ public void ItThrowsIfPackageRelativePathExceedsLimit()
Exception e = Assert.Throws(() =>
{
- MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath,
+ MsiSwixProject swixProject = new(msiItem, GetTestCaseDirectory(), BaseOutputPath,
new ReleaseVersion("6.0.100"),
chip: "x64", machineArch: "x64", productArch: "neutral");
});
@@ -38,23 +38,23 @@ public void ItThrowsIfPackageRelativePathExceedsLimit()
[WindowsOnlyFact]
public void ItOnlyIncludesDefinedPropertiesForMsiPackages()
{
- // Build to a different path to avoid any file read locks on the MSI from other tests
- // that can open it.
string packageVersion = "16.0.527";
- string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, Path.GetRandomFileName());
+ string outputDirectory = GetTestCaseDirectory();
+ string pkgDirectory = Path.Combine(outputDirectory, "pkg");
+ string msiDirectory = Path.Combine(outputDirectory, "msi");
string packagePath = Path.Combine(TestAssetsPath, $"microsoft.ios.templates.{packageVersion}.nupkg");
WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), packageVersion, WorkloadPackKind.Template, null);
- TemplatePackPackage pkg = new(p, packagePath, new[] { "x64" }, PackageRootDirectory);
+ TemplatePackPackage pkg = new(p, packagePath, ["x64"], pkgDirectory);
pkg.Extract();
- WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath);
+ WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetConfig, outputDirectory);
- ITaskItem msiItem = msi.Build(MsiOutputPath);
+ ITaskItem msiItem = msi.Build(msiDirectory);
msiItem.SetMetadata(Metadata.Platform, "x64");
Assert.Equal($"Microsoft.iOS.Templates.{packageVersion}", msiItem.GetMetadata(Metadata.SwixPackageId));
- MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath,
+ MsiSwixProject swixProject = new(msiItem, outputDirectory, BaseOutputPath,
new ReleaseVersion("6.0.100"),
chip: msiItem.GetMetadata(Metadata.Platform),
machineArch: DefaultValues.x64);
@@ -65,6 +65,5 @@ public void ItOnlyIncludesDefinedPropertiesForMsiPackages()
Assert.Contains("vs.package.chip=x64", msiSwr);
Assert.Contains("vs.package.machineArch=x64", msiSwr);
}
-
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
index 52fb73d78bf..b89834eebd7 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
@@ -2,7 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
+using System.Linq;
+using AwesomeAssertions;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.DotNet.Build.Tasks.Workloads.Msi;
+using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
+using WixToolset.Dtf.WindowsInstaller;
+using Xunit;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
{
@@ -11,11 +21,221 @@ public abstract class TestBase
public static readonly string BaseIntermediateOutputPath = Path.Combine(AppContext.BaseDirectory, "obj", Path.GetFileNameWithoutExtension(Path.GetTempFileName()));
public static readonly string BaseOutputPath = Path.Combine(AppContext.BaseDirectory, "bin", Path.GetFileNameWithoutExtension(Path.GetTempFileName()));
- public static readonly string MsiOutputPath = Path.Combine(BaseOutputPath, "msi");
public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets");
- public static readonly string WixToolsetPath = Path.Combine(TestAssetsPath, "wix");
+ public static readonly string TestOutputRoot = Path.Combine(AppContext.BaseDirectory, "TEST_OUTPUT");
- public static readonly string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg");
+ ///
+ /// Wix Toolset to use for tests..
+ ///
+ public static WixToolsetConfiguration WixToolsetConfig = WixToolsetConfiguration.Create(
+ ToolsetInfo.WixExePath, ToolsetInfo.HeatExePath,
+ ToolsetInfo.DependencyExt, ToolsetInfo.UtilExt, ToolsetInfo.UIExt);
+
+ ///
+ /// Item group containing WiX extensions. This is required by the public tasks and is similar
+ /// to how users would pass information about the extensions.
+ ///
+ public static ITaskItem[] WixExtensions = [
+ new TaskItem(ToolsetInfo.DependencyExt),
+ new TaskItem(ToolsetInfo.UtilExt),
+ new TaskItem(ToolsetInfo.UIExt)
+ ];
+
+ public static string MSBuildExePath;
+
+ ///
+ /// Returns a new, random directory for a test case.
+ ///
+ public string GetTestCaseDirectory() =>
+ Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+
+ ///
+ /// Verifies that the Upgrade table contains a row matching the provided expected values.
+ ///
+ protected static void ValidatedRelatedProduct(IEnumerable relatedProducts,
+ string expectedUpgradeCode, string expectedVersionMin,
+ string expectedVersionMax, int expectedAttributes, string expectedActionProperty)
+ {
+ relatedProducts.Should().Contain(r => string.Equals(r.UpgradeCode, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase) &&
+ r.VersionMin == expectedVersionMin &&
+ r.VersionMax == expectedVersionMax &&
+ r.ActionProperty == expectedActionProperty &&
+ r.Attributes == expectedAttributes);
+ }
+
+ ///
+ /// Verify that the CustomAction table contains the expected entries for setting DOTNETHOME when executing
+ /// under emulation on arm64.
+ ///
+ /// List of custom actions to validate.
+ /// The platform of the MSI to validate.
+ protected static void ValidateDotNetHomeCustomActions(IEnumerable customActions, string platform)
+ {
+ if (platform == "x64")
+ {
+ customActions.Should().Contain(c => c.Action == "Set_NON_NATIVE_ARCHITECTURE" &&
+ c.Source == "NON_NATIVE_ARCHITECTURE");
+ customActions.Should().Contain(c => c.Action == "Set_DOTNETHOME_NON_NATIVE_ARCHITECTURE" &&
+ c.Source == "DOTNETHOME");
+ }
+ else
+ {
+ customActions.Should().NotContain(c => c.Action == "Set_NON_NATIVE_ARCHITECTURE" &&
+ c.Source == "NON_NATIVE_ARCHITECTURE");
+ customActions.Should().NotContain(c => c.Action == "Set_DOTNETHOME_NON_NATIVE_ARCHITECTURE" &&
+ c.Source == "DOTNETHOME");
+ }
+ }
+
+ ///
+ /// Verify that the registry keys for the workload installation record exists. The records
+ /// are used by the .NET CLI to manage installs.
+ ///
+ /// The set of keys to verify.
+ /// The installation record key, for example, SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64\6.0.4.
+ /// The dependency provider key of the MSI.
+ /// The ProductCode of the MSI.
+ /// The UpgradeCode of the MSI.
+ /// The ProductVersion of the MSI.
+ /// The ProductLanguage of the MSI.
+ protected static void ValidateInstallationRecord(IEnumerable registryKeys,
+ string installationRecordKey, string expectedProviderKey, string expectedProductCode, string expectedUpgradeCode,
+ string expectedProductVersion,
+ string expectedProductLanguage = "#1033")
+ {
+ // Filter out the installation record keys. They should all be under HKLM (Root == 2).
+ var keys = registryKeys.Where(r => r.Key == installationRecordKey && r.Root == 2);
+
+ keys.Should().Contain(r =>
+ r.Name == "DependencyProviderKey" &&
+ r.Value == expectedProviderKey);
+ keys.Should().Contain(r =>
+ r.Name == "ProductCode" &&
+ string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase));
+ keys.Should().Contain(r =>
+ r.Name == "UpgradeCode" &&
+ string.Equals(r.Value, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase));
+ keys.Should().Contain(r =>
+ r.Name == "ProductVersion" &&
+ r.Value == expectedProductVersion);
+ keys.Should().Contain(r =>
+ r.Name == "ProductLanguage" &&
+ r.Value == expectedProductLanguage);
+ }
+
+ ///
+ /// Validates that the specified registry keys collection contains an entry for each provided pack key with the
+ /// expected properties.
+ ///
+ /// The collection of registry key records to validate. Each record is checked for the presence of the specified
+ /// pack keys.
+ /// The names of the pack keys to verify within the registry keys collection. Each key must correspond to a
+ /// registry entry with a root value of 2 and an empty value.
+ protected static void ValidatePackGroupInstallRecordKeys(IEnumerable registryKeys,
+ params string[] packKeys)
+ {
+ foreach (var key in packKeys)
+ {
+ registryKeys.Should().Contain(r => r.Key == key &&
+ r.Root == 2 &&
+ r.Name == "" &&
+ r.Value == "");
+ }
+ }
+
+ ///
+ /// Verify that the registry table contains entries for the dependency provider.
+ ///
+ /// Rows from the Registry table to validate.
+ /// The dependency provider key.
+ protected static void ValidateDependencyProviderKey(IEnumerable registryKeys, string dependencyProviderKey)
+ {
+ // Filter out the provider keys. The Root is expected to be -1 because the dependency
+ // provider extension can be used to author per-machine or per-user packages. If ALLUSERS is
+ // set to 1, the key will be written to HKLM, otherwise it's written to HKCU.
+ var keys = registryKeys.Where(r => r.Key == dependencyProviderKey && r.Root == -1);
+
+ // Dependency provider entries reference the ProductVersion and ProductName properties. These
+ // properties are set at install time.
+ keys.Should().Contain(r =>
+ r.Name == "Version" &&
+ r.Value == "[ProductVersion]");
+ keys.Should().Contain(r =>
+ r.Name == "DisplayName" &&
+ r.Value == "[ProductName]");
+ }
+
+ ///
+ /// Verify that the summary information stream matches the expected values for the given platform.
+ ///
+ /// The path to the MSI.
+ /// The target platform from the test.
+ protected static void ValidateSummaryInformation(string path, string platform)
+ {
+ using SummaryInfo si = new(path, enableWrite: false);
+
+ switch (platform)
+ {
+ case "x86":
+ Assert.Equal("Intel;1033", si.Template);
+ break;
+ case "x64":
+ Assert.Equal($"x64;1033", si.Template);
+ break;
+ case "arm64":
+ Assert.Equal($"Arm64;1033", si.Template);
+ break;
+ default: throw new Exception($"Invalid platform: {platform}");
+ }
+ }
+
+ ///
+ /// Compiles a SWIX project.
+ ///
+ /// The path of the SWIX project to build.
+ /// The file system path of the directory where the SWIX manifest will be generated
+ ///
+ protected static void BuildSwixProject(string projectPath, string manifestOutputPath)
+ {
+ string[] args = [$@"""{projectPath}""",
+ $@"/p:ManifestOutputPath={manifestOutputPath}",
+ $@"/p:SwixBuildTargets={ToolsetInfo.SwixTargetsPath}"
+ ];
+
+ var msbuildStartInfo = new ProcessStartInfo(MSBuildExePath, string.Join(" ", args))
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ };
+
+ var proc = Process.Start(msbuildStartInfo)!;
+ proc.WaitForExit();
+ }
+
+ static TestBase()
+ {
+ var vswhere = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
+ @"Microsoft Visual Studio\Installer\vswhere.exe");
+
+ if (!File.Exists(vswhere))
+ {
+ vswhere = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
+ @"Microsoft Visual Studio\Installer\vswhere.exe");
+ }
+
+ if (!File.Exists(vswhere))
+ {
+ Console.WriteLine("Skipping because vswhere.exe is unavailable on this machine.");
+ return;
+ }
+
+ var vsPath = Process.Start(new ProcessStartInfo(vswhere,
+ "-latest -requires Microsoft.Component.MSBuild -property installationPath")
+ { RedirectStandardOutput = true })!.StandardOutput.ReadToEnd().Trim();
+
+ MSBuildExePath = Path.Combine(vsPath, @"MSBuild\Current\Bin\MSBuild.exe");
+ }
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/ToolsetInfo.cs.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/ToolsetInfo.cs.pp
new file mode 100644
index 00000000000..0357d440957
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/ToolsetInfo.cs.pp
@@ -0,0 +1,18 @@
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
+{
+ // This class is generated by a tool.
+ public class ToolsetInfo
+ {
+ public static readonly string WixExePath = Path.Combine(AppContext.BaseDirectory, @"testassets\wix\net6.0\any\wix.exe");
+ public static readonly string HeatExePath = Path.Combine(AppContext.BaseDirectory, @"testassets\wix\heat\net472\x64\heat.exe");
+
+ public static readonly string DependencyExt = Path.Combine(AppContext.BaseDirectory, @"testassets\wixext\dependency\WixToolset.Dependency.wixext.dll");
+ public static readonly string UIExt = Path.Combine(AppContext.BaseDirectory, @"testassets\wixext\ui\WixToolset.UI.wixext.dll");
+ public static readonly string UtilExt = Path.Combine(AppContext.BaseDirectory, @"testassets\wixext\util\WixToolset.Util.wixext.dll");
+
+ public const string SwixTargetsPath = @"{SwixTargetsPath}";
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
index 5865ac44ac7..3c44284555f 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
@@ -84,6 +84,9 @@ public ITaskItem[] WorkloadManifestPackageFiles
set;
}
+ ///
+ /// When , multiple packs are combined into a single installer.
+ ///
public bool CreateWorkloadPackGroups
{
get;
@@ -100,12 +103,6 @@ public string PackageSource
set;
}
- public bool UseWorkloadPackGroupsForVS
- {
- get;
- set;
- }
-
///
/// If true, will skip creating MSIs for workload packs if they are part of a pack group
///
@@ -115,6 +112,9 @@ public bool SkipRedundantMsiCreation
set;
}
+ ///
+ /// If true, workload pack groups are built sequentially.
+ ///
public bool DisableParallelPackageGroupProcessing
{
get;
@@ -166,7 +166,8 @@ protected override bool ExecuteCore()
Dictionary manifestMsisByPlatform = new();
foreach (string platform in SupportedPlatforms)
{
- var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, EnableSideBySideManifests);
+ WorkloadManifestMsi manifestMsi = new(manifestPackage, platform, BuildEngine, WixToolsetConfig, BaseIntermediateOutputPath,
+ EnableSideBySideManifests, CreateWixPacks);
manifestMsisToBuild.Add(manifestMsi);
manifestMsisByPlatform[platform] = manifestMsi;
}
@@ -207,7 +208,7 @@ protected override bool ExecuteCore()
WorkloadPackGroupJson packGroupJson = null;
if (CreateWorkloadPackGroups)
{
- packGroupId = WorkloadPackGroupPackage.GetPackGroupID(workload.Id);
+ packGroupId = WorkloadPackGroupPackage.GetPackGroupId(workload.Id);
packGroupJson = new WorkloadPackGroupJson()
{
GroupPackageId = packGroupId,
@@ -289,7 +290,7 @@ protected override bool ExecuteCore()
string uniquePackGroupKey = string.Join("\r\n", kvp.Value.Select(p => p.PackagePath).OrderBy(p => p));
if (!packGroupPackages.TryGetValue(uniquePackGroupKey, out var groupPackage))
{
- groupPackage = new WorkloadPackGroupPackage(workload.Id);
+ groupPackage = new WorkloadPackGroupPackage(workload.Id, manifestPackage);
groupPackage.Packs.AddRange(kvp.Value);
packGroupPackages[uniquePackGroupKey] = groupPackage;
}
@@ -299,12 +300,7 @@ protected override bool ExecuteCore()
groupPackage.ManifestsPerPlatform[platform] = new();
}
groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage);
- }
-
- foreach (var manifestMsi in manifestMsisByPlatform.Values)
- {
- manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList);
- }
+ }
}
// Finally, add a component for the workload in Visual Studio.
@@ -327,81 +323,96 @@ protected override bool ExecuteCore()
}
}
}
+
+ if (CreateWorkloadPackGroups)
+ {
+ foreach (var manifestMsi in manifestMsisByPlatform.Values)
+ {
+ manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList);
+ }
+ }
}
+ // Deduplicate packages and extract them. Building and extracting in parallel can cause issues as the same
+ // source package can be shared across multiple workloads and platforms.
+ Parallel.ForEach(buildData.Values.Select(d => d.Package).Distinct(), package =>
+ {
+ Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, package.PackagePath));
+ package.Extract();
+ });
+
List msiItems = new();
List swixProjectItems = new();
- _ = Parallel.ForEach(buildData.Values, data =>
+ if (!CreateWorkloadPackGroups)
{
- // Extract the contents of the workload pack package.
- Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath));
- data.Package.Extract();
-
- // Enumerate over the platforms and build each MSI once.
- _ = Parallel.ForEach(data.FeatureBands.Keys, platform =>
+ _ = Parallel.ForEach(buildData.Values, data =>
{
- WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath);
- ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions);
+ // Extract the contents of the workload pack package.
+ Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath));
+ data.Package.Extract();
- // Generate a .csproj to package the MSI and its manifest for CLI installs.
- MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
- msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
-
- lock (msiItems)
+ // Enumerate over the platforms and build each MSI once.
+ _ = Parallel.ForEach(data.FeatureBands.Keys, platform =>
{
- msiItems.Add(msiOutputItem);
- }
+ WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetConfig, BaseIntermediateOutputPath, CreateWixPacks);
+ ITaskItem msiOutputItem = msi.Build(MsiOutputPath);
- foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform])
- {
- // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch
- if (_supportsMachineArch[sdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64))
- {
- MsiSwixProject swixProject = _supportsMachineArch[sdkFeatureBand] ?
- new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) :
- new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio);
- string swixProj = swixProject.Create();
+ // Generate a .csproj to package the MSI and its manifest for CLI installs.
+ MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
+ msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
- ITaskItem swixProjectItem = new TaskItem(swixProj);
- swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}");
- swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack);
- swixProjectItem.SetMetadata(Metadata.IsPreview, "false");
+ lock (msiItems)
+ {
+ msiItems.Add(msiOutputItem);
+ }
- lock (swixProjectItems)
+ foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform])
+ {
+ // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch
+ if (_supportsMachineArch[sdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64))
{
- swixProjectItems.Add(swixProjectItem);
+ MsiSwixProject swixProject = _supportsMachineArch[sdkFeatureBand] ?
+ new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) :
+ new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio);
+ string swixProj = swixProject.Create();
+
+ ITaskItem swixProjectItem = new TaskItem(swixProj);
+ swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}");
+ swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack);
+ swixProjectItem.SetMetadata(Metadata.IsPreview, "false");
+ swixProjectItem.SetMetadata(Metadata.Platform, msiOutputItem.GetMetadata(Metadata.Platform));
+ swixProjectItem.SetMetadata(Metadata.SwixPackageId, msiOutputItem.GetMetadata(Metadata.SwixPackageId));
+
+ lock (swixProjectItems)
+ {
+ swixProjectItems.Add(swixProjectItem);
+ }
}
}
- }
+ });
});
- });
-
- // Parallel processing of pack groups was causing file access errors for heat in an earlier version of this code
- // So we support a flag to disable the parallelization if that starts happening again
- PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroupPackages.Values, packGroup =>
+ }
+ else
{
- foreach (var pack in packGroup.Packs)
- {
- pack.Extract();
- }
-
- foreach (var platform in packGroup.ManifestsPerPlatform.Keys)
+ // Parallel processing of pack groups was causing file access errors for heat in an earlier version of this code
+ // So we support a flag to disable the parallelization if that starts happening again
+ PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroupPackages.Values, packGroup =>
{
- WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath);
- ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions);
+ foreach (var platform in packGroup.ManifestsPerPlatform.Keys)
+ {
+ WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetConfig, BaseIntermediateOutputPath, CreateWixPacks);
+ ITaskItem msiOutputItem = msi.Build(MsiOutputPath);
- // Generate a .csproj to package the MSI and its manifest for CLI installs.
- MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
- msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
+ // Generate a .csproj to package the MSI and its manifest for CLI installs.
+ MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
+ msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
- lock (msiItems)
- {
- msiItems.Add(msiOutputItem);
- }
+ lock (msiItems)
+ {
+ msiItems.Add(msiOutputItem);
+ }
- if (UseWorkloadPackGroupsForVS)
- {
PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage =>
{
// Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch
@@ -416,6 +427,8 @@ protected override bool ExecuteCore()
swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{manifestPackage.SdkFeatureBand}");
swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack);
swixProjectItem.SetMetadata(Metadata.IsPreview, "false");
+ swixProjectItem.SetMetadata(Metadata.Platform, msiOutputItem.GetMetadata(Metadata.Platform));
+ swixProjectItem.SetMetadata(Metadata.SwixPackageId, msiOutputItem.GetMetadata(Metadata.SwixPackageId));
lock (swixProjectItems)
{
@@ -424,14 +437,14 @@ protected override bool ExecuteCore()
}
});
}
- }
- });
+ });
+ }
// Generate MSIs for the workload manifests along with a .csproj to package the MSI and a SWIX project for
// Visual Studio.
_ = Parallel.ForEach(manifestMsisToBuild, msi =>
{
- ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions);
+ ITaskItem msiOutputItem = msi.Build(MsiOutputPath);
// Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch
if (_supportsMachineArch[msi.Package.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64))
@@ -445,6 +458,8 @@ protected override bool ExecuteCore()
swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}");
swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiManifest);
swixProjectItem.SetMetadata(Metadata.IsPreview, "false");
+ swixProjectItem.SetMetadata(Metadata.Platform, msiOutputItem.GetMetadata(Metadata.Platform));
+ swixProjectItem.SetMetadata(Metadata.SwixPackageId, msiOutputItem.GetMetadata(Metadata.SwixPackageId));
lock (swixProjectItems)
{
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs
index 97ea915591e..3fa0d413a30 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs
@@ -51,7 +51,7 @@ protected override bool ExecuteCore()
foreach (string platform in SupportedPlatforms)
{
var workloadSetMsi = new WorkloadSetMsi(workloadSetPackage, platform, BuildEngine,
- WixToolsetPath, BaseIntermediateOutputPath);
+ WixToolsetConfig, BaseIntermediateOutputPath, CreateWixPacks);
workloadSetMsisToBuild.Add(workloadSetMsi);
}
@@ -67,7 +67,7 @@ protected override bool ExecuteCore()
_ = Parallel.ForEach(workloadSetMsisToBuild, msi =>
{
- ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions);
+ ITaskItem msiOutputItem = msi.Build(MsiOutputPath);
// Generate a .csproj to package the MSI and its manifest for CLI installs.
MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
index 7fd2709c466..52f700f6250 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
@@ -8,6 +8,15 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads
///
internal static class DefaultValues
{
+ ///
+ /// Maximum size of an MSI in bytes.
+ ///
+ ///
+ /// Workload MSIs are distributed in NuGet packages and cannot exceed the maximum size of a NuGet package (250 MB). The limit
+ /// is set to 245 MB to account for package metadata, signatures, etc.
+ ///
+ public const int MaxMsiSize = 256901120;
+
///
/// Prefix used in Visual Studio for SWIX based package group.
///
@@ -58,5 +67,49 @@ internal static class DefaultValues
/// A value indicating that the SWIX project creates an MSI package for a workload set.
///
public static readonly string PackageTypeMsiWorkloadSet = "msi-workload-set";
+
+ ///
+ /// A value indicating the MSI represents a workload manifest.
+ ///
+ public static readonly string ManifestMsi = "manifest";
+
+ ///
+ /// A value indicating the MSI represents a workload pack.
+ ///
+ public static readonly string WorkloadPackMsi = "pack";
+
+ ///
+ /// A value indicating the MSI represents a workload set.
+ ///
+ public static readonly string WorkloadSetMsi = "workload-set";
+
+ ///
+ /// A value indicating the MSI represents a workload pack group.
+ ///
+ public static readonly string WorkloadPackGroupMsi = "pack-group";
+
+ ///
+ /// Default values specific to SWIX authoring.
+ ///
+ internal static class Swix
+ {
+ public static readonly string Yes = "yes";
+
+ public static readonly string No = "no";
+ }
+
+ internal static class Wix
+ {
+ ///
+ /// Default LCID value for the ProductLanguage property.
+ ///
+ public const string Language = "1033";
+
+ ///
+ /// Default preprocessor variable to use for substituing File@Source when harvesting a directory with Heat.
+ ///
+ public const string SourceVariableName = "SourceDir";
+ }
+
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
index 61c5f9f553c..802cd8c3e6b 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
@@ -73,6 +73,7 @@ static EmbeddedTemplates()
{ "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" },
{ "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" },
{ "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" },
+ { "Productv5.wxs", $"{ns}.MsiTemplate.Productv5.wxs" },
{ "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" },
{ "Variables.wxi", $"{ns}.MsiTemplate.Variables.wxi" },
@@ -81,7 +82,7 @@ static EmbeddedTemplates()
{ $"component.swr", $"{ns}.SwixTemplate.component.swr" },
{ $"component.res.swr", $"{ns}.SwixTemplate.component.res.swr" },
{ $"component.swixproj", $"{ns}.SwixTemplate.component.swixproj" },
- { $"manifest.vsmanproj", $"{ns}.SwixTempalte.manifest.vsmanproj" },
+ { $"manifest.vsmanproj", $"{ns}.SwixTemplate.manifest.vsmanproj" },
{ $"packageGroup.swr", $"{ns}.SwixTemplate.packageGroup.swr" },
{ $"packageGroup.swixproj", $"{ns}.SwixTemplate.packageGroup.swixproj" },
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
index 229bb00056e..ea1edc18a61 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
@@ -36,9 +36,15 @@ internal static class Metadata
public static readonly string SdkFeatureBand = nameof(SdkFeatureBand);
public static readonly string ShortName = nameof(ShortName);
public static readonly string SourcePackage = nameof(SourcePackage);
+ public static readonly string SourcePath = nameof(SourcePath);
public static readonly string SwixPackageId = nameof(SwixPackageId);
public static readonly string SwixProject = nameof(SwixProject);
+ ///
+ /// The file system path of the generated wixpack archive used for signing.
+ ///
+ public static readonly string WixPack = nameof(WixPack);
+
///
/// Metadata describing whether the VS authoring supports the machineArch SWIX property.
///
@@ -52,11 +58,5 @@ internal static class Metadata
public static readonly string Title = nameof(Title);
public static readonly string Version = nameof(Version);
-
- ///
- /// Metadata used by tasks generating MSIs to specify the path of .wixobj files produced by
- /// the compiler.
- ///
- public static readonly string WixObj = nameof(WixObj);
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj
index 4a7cb2e5c72..a954943ead1 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj
@@ -1,4 +1,4 @@
-
+
$(NetMinimum)
@@ -32,12 +32,8 @@
-
-
-
-
-
-
+
+
@@ -46,26 +42,22 @@
+
-
-
-
+
+
-
-
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs
new file mode 100644
index 00000000000..a9017cd7384
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using WixToolset.Dtf.WindowsInstaller;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Defines a single row inside the Component table of an MSI.
+ ///
+ public class ComponentRow
+ {
+ ///
+ /// Identifies the component record.
+ ///
+ public string Component
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// A string GUID unique to this component, version, and language.
+ ///
+ public string ComponentId
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// External key of an entry in the Directory table.
+ ///
+ public string Directory_
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Bit flags that specifies options for remote execution.
+ ///
+ public int Attributes
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Conditional statement that determines whether the component will be installed.
+ ///
+ public string Condition
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// This value points to a file or folder belonging to the component that the installer uses to
+ /// detect the component.
+ ///
+ public string KeyPath
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Creates a new instance from the given .
+ ///
+ /// The record to use.
+ /// A new component row.
+ public static ComponentRow Create(Record componentRecord) =>
+ new ComponentRow
+ {
+ Component = componentRecord.GetString("Component"),
+ ComponentId = componentRecord.GetString("ComponentId"),
+ Directory_ = componentRecord.GetString("Directory_"),
+ Attributes = componentRecord.GetInteger("Attributes"),
+ Condition = componentRecord.GetString("Condition"),
+ KeyPath = componentRecord.GetString("KeyPath"),
+ };
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/CustomActionRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/CustomActionRow.wix.cs
new file mode 100644
index 00000000000..4cca1cfb3c8
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/CustomActionRow.wix.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using WixToolset.Dtf.WindowsInstaller;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Defines a single row from the CustomAction table of an MSI.
+ ///
+ public class CustomActionRow
+ {
+ ///
+ /// Name of the action. The action normally appears in a sequence table unless it is called by another custom action.
+ ///
+ public string Action
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// A field of flag bits specifying the basic type of custom action and options.
+ ///
+ public int Type
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// A property name or external key into another table
+ ///
+ public string Source
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// An execution parameter that depends on the basic type of custom action.
+ ///
+ public string Target
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Creates a new instance from the specified .
+ ///
+ /// The custom action record obtained from querying the MSI CustomAction table.
+ /// A single custom action row.
+ public static CustomActionRow Create(Record customActionRecord)
+ {
+ return new CustomActionRow
+ {
+ Action = (string)customActionRecord["Action"],
+ Type = (int)customActionRecord["Type"],
+ Source = (string)customActionRecord["Source"],
+ Target = (string)customActionRecord["Target"],
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs
index 057f660adaa..86d0896a649 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Deployment.WindowsInstaller;
+using WixToolset.Dtf.WindowsInstaller;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs
index 30fac6f1b1f..d3701d29328 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Deployment.WindowsInstaller;
+using WixToolset.Dtf.WindowsInstaller;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/IWorkloadPackageMetadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/IWorkloadPackageMetadata.cs
new file mode 100644
index 00000000000..ce7779c85b7
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/IWorkloadPackageMetadata.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using NuGet.Versioning;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Interface representing metadata associated with workload packages and used to generate other artifacts such as
+ /// MSIs, SWIX packages for Visual Studio or payload packages for NuGet that wrap the workload MSIs.
+ ///
+ public interface IWorkloadPackageMetadata
+ {
+ public string Id
+ {
+ get;
+ }
+
+ public NuGetVersion PackageVersion
+ {
+ get;
+ }
+
+ public Version MsiVersion
+ {
+ get;
+ }
+
+ public string Authors
+ {
+ get;
+ }
+
+ public string Copyright
+ {
+ get;
+ }
+
+ public string Description
+ {
+ get;
+ }
+
+ public string Title
+ {
+ get;
+ }
+
+ public string? LicenseUrl
+ {
+ get;
+ }
+ public string ProjectUrl
+ {
+ get;
+ }
+
+ public string SwixPackageId
+ {
+ get;
+ }
+ }
+}
+
+#nullable disable
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs
index 0e5c1df333c..c99f51cdbeb 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs
@@ -5,17 +5,32 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
+using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
- internal abstract class MsiBase
+ ///
+ /// Base class for workload MSIs templates.
+ ///
+ internal abstract class MsiBase : WorkloadTemplateBase
{
+ ///
+ /// MSBuild tool task for running the WiX CLI.
+ ///
+ private WixToolTask _wixToolTask;
+
+ ///
+ /// The base registry key where all workload records are written.
+ ///
+ protected const string InstallRecordBaseKey = @"SOFTWARE\Microsoft\dotnet";
+
///
/// Replacement token for license URLs in the generated EULA.
///
@@ -40,7 +55,7 @@ internal abstract class MsiBase
///
/// Metadata for the MSI such as package ID, version, author information, etc.
///
- public MsiMetadata Metadata
+ public IWorkloadPackageMetadata Metadata
{
get;
}
@@ -59,9 +74,9 @@ protected IBuildEngine BuildEngine
}
///
- /// The directory where the compiler output (.wixobj files) will be generated.
+ /// When , a wixpack archive will be generated that can be used to sign the MSI.
///
- protected string CompilerOutputPath
+ protected bool CreateWixPack
{
get;
}
@@ -74,6 +89,16 @@ protected string BaseIntermediateOutputPath
get;
}
+ ///
+ /// The registry key for tracking installation records used by the CLI and
+ /// and finalizer.
+ ///
+ protected string InstallationRecordKey
+ {
+ get;
+ init;
+ }
+
///
/// Gets the value to use for the manufacturer.
///
@@ -82,6 +107,23 @@ protected string BaseIntermediateOutputPath
Metadata.Authors :
DefaultValues.Manufacturer;
+ ///
+ /// The package type represented by the MSI.
+ ///
+ protected abstract string? MsiPackageType
+ {
+ get;
+ }
+
+ ///
+ /// The provider key name used to manage MSI dependents.
+ ///
+ protected string ProviderKeyName
+ {
+ get;
+ init;
+ }
+
///
/// The platform of the MSI.
///
@@ -90,25 +132,36 @@ protected string Platform
get;
}
+ ///
+ /// The ProductCode of the MSI.
+ ///
+ protected Guid ProductCode
+ {
+ get;
+ init;
+ }
+
///
/// The filename of the MSI file to generate.
///
- protected string OutputName => $"{Utils.GetTruncatedHash(BaseOutputName, HashAlgorithmName.SHA256)}-{Platform}.msi";
+ protected string OutputName =>
+ $"{Utils.GetTruncatedHash(BaseOutputName, HashAlgorithmName.SHA256)}-{Platform}.msi";
///
- /// The directory where the WiX source code will be generated.
+ /// Toolset configuration to use to invoke the WiX CLI and related tools.
///
- protected string WixSourceDirectory
+ protected WixToolsetConfiguration WixToolsetConfig
{
get;
}
///
- /// The directory containing the WiX toolset binaries.
+ /// The UpgradeCode of the MSI.
///
- protected string WixToolsetPath
+ protected Guid UpgradeCode
{
get;
+ init;
}
///
@@ -117,27 +170,126 @@ protected string WixToolsetPath
///
public Dictionary NuGetPackageFiles { get; set; } = new();
- public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath,
- string platform, string baseIntermediateOutputPath)
+ public MsiBase(IWorkloadPackageMetadata metadata, IBuildEngine buildEngine, WixToolsetConfiguration wixToolsetConfig,
+ string platform, string baseIntermediateOutputPath, bool createWixPack = true) : base(baseIntermediateOutputPath)
{
BuildEngine = buildEngine;
- WixToolsetPath = wixToolsetPath;
+ Metadata = metadata;
+ WixToolsetConfig = wixToolsetConfig;
+ CreateWixPack = createWixPack;
+
+ InstallationRecordKey = InstallRecordBaseKey;
Platform = platform;
BaseIntermediateOutputPath = baseIntermediateOutputPath;
+ ProviderKeyName = "";
- // Candle expects the output path to be terminated with a single '\'.
- CompilerOutputPath = Utils.EnsureTrailingSlash(Path.Combine(baseIntermediateOutputPath, "wixobj", metadata.Id, $"{metadata.PackageVersion}", platform));
- WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", metadata.Id, $"{metadata.PackageVersion}", platform);
- Metadata = metadata;
+ ProductCode = Guid.NewGuid();
+ ReplacementTokens[MsiTokens.__MANUFACTURER__] = Manufacturer;
+ ReplacementTokens[MsiTokens.__NAME__] = GetProductName(Platform);
+ ReplacementTokens[MsiTokens.__PACKAGE_ID__] = metadata.Id;
+ ReplacementTokens[MsiTokens.__PACKAGE_VERSION__] = metadata.PackageVersion.ToString();
+ ReplacementTokens[MsiTokens.__PRODUCTCODE__] = ProductCode.ToString("B");
+ ReplacementTokens[MsiTokens.__VERSION__] = metadata.MsiVersion.ToString();
+
+ SourcePath = Path.Combine(SourcePath, "wix", metadata.Id, $"{metadata.PackageVersion}", platform);
+
+ _wixToolTask = new WixToolTask(buildEngine, wixToolsetConfig);
+ }
+
+ ///
+ /// Create the initial set of source files required to build the MSI.
+ ///
+ ///
+ /// Derived classes should call this method inside their own Create method to ensure the base product source file is generated.
+ ///
+ /// A WixDocument representing the Product.wxs source file.
+ protected WixDocument CreateProduct()
+ {
+ // Generate the EULA on disk and include its path as a replacement token before
+ // the primary .wxs template is created since AddFile always applies the replacement tokens.
+ ReplacementTokens[MsiTokens.__EULA_RTF__] = GenerateEula();
+ var productDoc = new WixDocument(AddFile("Product.wxs"));
+
+ // Add additional authoring to set DOTNETHOME property based on the native machine
+ // type. This is needed to support non-native installs like x64 on arm64.
+ if (Platform == "x64")
+ {
+ AddFile("dotnethome_x64.wxs");
+ productDoc.AddCustomActionRef("Set_DOTNETHOME_NON_NATIVE_ARCHITECTURE");
+ }
+
+ return productDoc;
}
+ ///
+ /// Creates a RegistryKey for the workload installation record.
+ ///
+ /// The RegistryKey element for the workload installation record.
+ protected virtual XElement CreateInstallationRecord() =>
+ WixDocument.CreateRegistryKey(InstallationRecordKey, "HKLM")
+ .AddRegistryValue("DependencyProviderKey", ProviderKeyName, keyPath: true)
+ .AddRegistryValue("ProductCode", $"{ProductCode:B}")
+ .AddRegistryValue("UpgradeCode", $"{UpgradeCode:B}")
+ .AddRegistryValue("ProductVersion", Metadata.MsiVersion.ToString())
+ .AddRegistryValue("ProductLanguage", DefaultValues.Wix.Language, type: "integer");
+
///
/// Produces an MSI and returns a task item with metadata about the MSI.
///
/// The directory where the MSI will be generated.
- /// A set of internal consistency evaluators to suppress or .
/// An item representing the built MSI.
- public abstract ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions);
+ public ITaskItem Build(string outputPath)
+ {
+ // Ensure that all the sources are generated before building the MSI.
+ Create();
+
+ _wixToolTask.Architecture = Platform;
+ _wixToolTask.OutputPath = Path.Combine(outputPath, OutputName);
+ _wixToolTask.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallerPlatform, Platform);
+ _wixToolTask.AddSourceFiles(Files);
+
+ if (!_wixToolTask.Execute())
+ {
+ throw new Exception(string.Format(Strings.FailedToCompileMsi, _wixToolTask.GetWixCommandLine()));
+ }
+
+ TaskItem msiItem = new TaskItem(_wixToolTask.OutputPath);
+ msiItem.SetMetadata(Workloads.Metadata.Platform, Platform);
+ msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}");
+ msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId);
+ msiItem.SetMetadata(Workloads.Metadata.PackageType, MsiPackageType);
+ msiItem.SetMetadata(Workloads.Metadata.SourcePath, SourcePath);
+
+ // NuGet limits package sizes to 250MB and Visual Studio has seen degraded performance for online installs
+ // when files exceed this. The actual limit is capped below 250MB to account for additional metadata and files
+ // we may include in the NuGet package that will wrap the workload MSI.
+ var fi = new FileInfo(msiItem.ItemSpec);
+ if (fi.Length > DefaultValues.MaxMsiSize)
+ {
+ throw new Exception($"The generated MSI, {msiItem.ItemSpec}, exceeded the maximum allowed size ({DefaultValues.MaxMsiSize} bytes).");
+ }
+
+ // Create the JSON manifest for CLI based installations.
+ string msiJsonPath = MsiProperties.Create(msiItem.ItemSpec);
+ NuGetPackageFiles[Path.GetFullPath(msiJsonPath)] = @"\data\msi.json";
+ NuGetPackageFiles[msiItem.GetMetadata(Workloads.Metadata.FullPath)] = @"\data";
+ NuGetPackageFiles["LICENSE.TXT"] = @"\";
+
+ if (CreateWixPack)
+ {
+ var createWixPackTask = _wixToolTask.GetCreateWixBuildWixpackTask(BuildEngine, IntermediateOutputPath,
+ OutputPath, _wixToolTask.OutputPath, Path.Combine(SourcePath, "wixpack"));
+
+ if (!createWixPackTask.Execute())
+ {
+ throw new Exception("Failed to generate wixpack.");
+ }
+
+ msiItem.SetMetadata(Workloads.Metadata.WixPack, createWixPackTask.OutputFile);
+ }
+
+ return msiItem;
+ }
///
/// Gets the platform specific ProductName MSI property.
@@ -152,101 +304,54 @@ protected string GetProductName(string platform) =>
///
protected string GenerateEula()
{
- string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf");
+ string eulaRtf = Path.Combine(SourcePath, "eula.rtf");
+ Directory.CreateDirectory(SourcePath);
File.WriteAllText(eulaRtf, s_eula.Replace(__LICENSE_URL__, Metadata.LicenseUrl));
return eulaRtf;
}
///
- /// Creates a new compiler tool task and configures some common extensions and preprocessor
- /// variables.
+ /// Invokes Heat to harvest a directory and creates a ComponentGroupRef element that can be inserted into
+ /// the generated source.
///
- ///
- protected CompilerToolTask CreateDefaultCompiler()
+ /// The file system of the directory to harvest.
+ /// The directory reference to use for root directories.
+ /// The preprocessor variable to use for substituting File@Source.
+ /// A string containing the component group ID associated with the harvested files.
+ /// If Heat failed to execute.
+ /// Starting with v5, WiX supports a Files element that can be used to perform simple harvesting using
+ /// globs. The authoring is generated in memory instead of a separate source file, making it incompatible with
+ /// wixpacks Arcade uses for signing.
+ ///
+ protected string HarvestDirectory(string sourcePath, string directoryReference,
+ string sourceVariableName = DefaultValues.Wix.SourceVariableName)
{
- CompilerToolTask candle = new(BuildEngine, WixToolsetPath, CompilerOutputPath, Platform);
-
- // Required extension to parse the dependency provider authoring.
- candle.AddExtension(WixExtensions.WixDependencyExtension);
-
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula());
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer);
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Metadata.Id);
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform);
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform));
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}");
-
- return candle;
- }
+ // Generate a random component group ID. The generated ComponentGroupRef XML element will have the same ID
+ // and can then be inserted into any element that supports havinge a ComponentGroupRef as a child, for example,
+ // a Feature element.
+ string componentGroupId = $"cg{Guid.NewGuid():N}";
+ string outputFile = Path.Combine(SourcePath, componentGroupId + ".wxs");
- ///
- /// Links the MSI using the output from the WiX compiler using a default set of WiX extensions.
- ///
- /// The path where the output of the compiler (.wixobj files) will be generated.
- /// The full path of the MSI to create during linking.
- /// A set of internal consistency evaluators to suppress. May be .
- /// An for the MSI that was created.
- ///
- protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions = null)
- {
- return Link(compilerOutputPath, outputFile, iceSuppressions, WixExtensions.WixDependencyExtension,
- WixExtensions.WixUIExtension, WixExtensions.WixUtilExtension);
- }
-
- ///
- /// Links the MSI using the output from the WiX compiler and a set of WiX extensions.
- ///
- /// The path where the output of the compiler (.wixobj files) can be found.
- /// The full path of the MSI to create during linking.
- /// A set of internal consistency evaluators to suppress. May be .
- /// A list of WiX extensions to include when linking the MSI.
- /// An for the MSI that was created.
- ///
- protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions, params string[] wixExtensions)
- {
- // Link the MSI. The generated filename contains the semantic version (excluding build metadata) and platform.
- // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add
- // the platform again.
- LinkerToolTask light = new(BuildEngine, WixToolsetPath)
+ HarvesterToolTask heat = new(BuildEngine, WixToolsetConfig)
{
+ ComponentGroupName = componentGroupId,
+ DirectoryReference = directoryReference,
OutputFile = outputFile,
- SourceFiles = Directory.EnumerateFiles(compilerOutputPath, "*.wixobj"),
- SuppressIces = iceSuppressions == null ? null : string.Join(";", iceSuppressions.Select(i => i.ItemSpec))
+ Platform = this.Platform,
+ SourceDirectory = sourcePath,
+ SourceVariableName = sourceVariableName
};
- foreach (string wixExtension in wixExtensions)
- {
- light.AddExtension(wixExtension);
- }
-
- if (!light.Execute())
+ if (!heat.Execute())
{
- throw new Exception(Strings.FailedToLinkMsi);
+ throw new Exception(Strings.HeatFailedToHarvest);
}
- TaskItem msiItem = new TaskItem(light.OutputFile);
+ AddSourceFile(outputFile);
+ _wixToolTask.AddPreprocessorDefinition(sourceVariableName, sourcePath);
- // Return a task item that contains all the information about the generated MSI.
- msiItem.SetMetadata(Workloads.Metadata.Platform, Platform);
- msiItem.SetMetadata(Workloads.Metadata.WixObj, compilerOutputPath);
- msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}");
- msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId);
-
- return msiItem;
- }
-
- protected void AddDefaultPackageFiles(ITaskItem msi)
- {
- NuGetPackageFiles[msi.GetMetadata(Workloads.Metadata.FullPath)] = @"\data";
-
- // Create the JSON manifest for CLI based installations.
- string msiJsonPath = MsiProperties.Create(msi.ItemSpec);
- NuGetPackageFiles[Path.GetFullPath(msiJsonPath)] = "\\data\\msi.json";
-
- NuGetPackageFiles["LICENSE.TXT"] = @"\";
+ return componentGroupId;
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs
index 1091751ed95..1f39d8b3ab1 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs
@@ -8,19 +8,54 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
///
internal class MsiDirectories
{
+ ///
+ /// Directory reference for the .NET installation directory under Program Files.
+ ///
+ public static readonly string DOTNETHOME = nameof(DOTNETHOME);
+
+ ///
+ /// Directory reference for the default workload pack installation directory.
+ ///
+ public static readonly string InstallDir = nameof(InstallDir);
+
///
/// The directory reference to use when harvesting the package contents for upgradable manifest installers
///
- public static readonly string ManifestIdDirectory = "ManifestIdDir";
+ public static readonly string ManifestIdDir = nameof(ManifestIdDir);
///
/// Directory reference to use when harvesting the package contents for SxS manifest installers.
///
- public static readonly string ManifestVersionDirectory = "ManifestVersionDir";
+ public static readonly string ManifestVersionDir = nameof(ManifestVersionDir);
+
+ ///
+ /// Directory reference for the workload pack package.
+ ///
+ public static readonly string PackageDir = nameof(PackageDir);
+
+ ///
+ /// Directory reference for the SDK manifests directory.
+ ///
+ public static readonly string SdkManifestDir = nameof(SdkManifestDir);
+
+ ///
+ /// Directory reference for the SDK feature band version directory.
+ ///
+ public static readonly string SdkFeatureBandVersionDir = nameof(SdkFeatureBandVersionDir);
+
+ ///
+ /// Directory reference for the workload pack package version.
+ ///
+ public static readonly string VersionDir = nameof(VersionDir);
+
+ ///
+ /// Directory reference for the root workload set directory.
+ ///
+ public static readonly string WorkloadSetsDir = nameof(WorkloadSetsDir);
///
/// Directory reference to use when harvesting package contents for workload sets.
///
- public static readonly string WorkloadSetVersionDirectory = "WorkloadSetVersionDir";
+ public static readonly string WorkloadSetVersionDir = nameof(WorkloadSetVersionDir);
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs
index 9f09b8a2246..73205bf35b9 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs
@@ -13,16 +13,9 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
///
/// Describes a project to package an MSI and its JSON manifest into a NuGet package.
///
- internal class MsiPayloadPackageProject : ProjectTemplateBase
+ internal class MsiPayloadPackageProject : WorkloadTemplateBase
{
- ///
- protected override string ProjectSourceDirectory
- {
- get;
- }
-
- ///
- protected override string ProjectFile
+ private string ProjectFile
{
get;
}
@@ -30,32 +23,32 @@ protected override string ProjectFile
// Key: path to file, value: path in package
public Dictionary PackageContents { get; set; } = new();
- public MsiPayloadPackageProject(MsiMetadata package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, Dictionary packageContents) :
- base(baseIntermediateOutputPath, baseOutputPath)
+ public MsiPayloadPackageProject(IWorkloadPackageMetadata packageMetadata, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, Dictionary packageContents) :
+ base(baseIntermediateOutputPath)
{
string platform = msi.GetMetadata(Metadata.Platform);
- ProjectSourceDirectory = Path.Combine(SourceDirectory, "msiPackage", platform, package.Id);
+ SourcePath = Path.Combine(SourcePath, "msiPackage", platform, packageMetadata.Id);
ProjectFile = "msi.csproj";
PackageContents = packageContents;
- ReplacementTokens[PayloadPackageTokens.__AUTHORS__] = package.Authors;
- ReplacementTokens[PayloadPackageTokens.__COPYRIGHT__] = package.Copyright;
- ReplacementTokens[PayloadPackageTokens.__DESCRIPTION__] = package.Description;
- ReplacementTokens[PayloadPackageTokens.__PACKAGE_ID__] = $"{package.Id}.Msi.{platform}";
- ReplacementTokens[PayloadPackageTokens.__PACKAGE_PROJECT_URL__] = package.ProjectUrl;
- ReplacementTokens[PayloadPackageTokens.__PACKAGE_VERSION__] = $"{package.PackageVersion}";
+ ReplacementTokens[PayloadPackageTokens.__AUTHORS__] = packageMetadata.Authors;
+ ReplacementTokens[PayloadPackageTokens.__COPYRIGHT__] = packageMetadata.Copyright;
+ ReplacementTokens[PayloadPackageTokens.__DESCRIPTION__] = packageMetadata.Description;
+ ReplacementTokens[PayloadPackageTokens.__PACKAGE_ID__] = $"{packageMetadata.Id}.Msi.{platform}";
+ ReplacementTokens[PayloadPackageTokens.__PACKAGE_PROJECT_URL__] = packageMetadata.ProjectUrl;
+ ReplacementTokens[PayloadPackageTokens.__PACKAGE_VERSION__] = $"{packageMetadata.PackageVersion}";
}
///
public override string Create()
{
- string msiCsproj = EmbeddedTemplates.Extract("msi.csproj", ProjectSourceDirectory);
+ AddFile("Icon.png", replaceTokens: false);
+ AddFile("LICENSE.TXT", replaceTokens: false);
+ string msiCsproj = AddFile("msi.csproj");
- Utils.StringReplace(msiCsproj, ReplacementTokens, Encoding.UTF8);
-
- var proj = XDocument.Load(msiCsproj);
- var itemGroup = proj.Root.Element("ItemGroup");
+ var projectDocument = XDocument.Load(msiCsproj);
+ var itemGroup = projectDocument.Root.Element("ItemGroup");
foreach (var packageFile in PackageContents)
{
itemGroup.Add(new XElement("None",
@@ -63,10 +56,7 @@ public override string Create()
new XAttribute("Pack", "true"),
new XAttribute("PackagePath", packageFile.Value)));
}
- proj.Save(msiCsproj);
-
- EmbeddedTemplates.Extract("Icon.png", ProjectSourceDirectory);
- EmbeddedTemplates.Extract("LICENSE.TXT", ProjectSourceDirectory);
+ projectDocument.Save(msiCsproj);
return msiCsproj;
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs
new file mode 100644
index 00000000000..5d3504019d0
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Replacement tokens used to generate MSI source files.
+ ///
+ internal class MsiTokens
+ {
+ public const string __MANUFACTURER__ = nameof(__MANUFACTURER__);
+ public const string __NAME__ = nameof(__NAME__);
+ public const string __PACKAGE_ID__ = nameof(__PACKAGE_ID__);
+ public const string __PACKAGE_VERSION__ = nameof(__PACKAGE_VERSION__);
+ public const string __PRODUCTCODE__ = nameof(__PRODUCTCODE__);
+ public const string __PROVIDER_KEY_NAME__ = nameof(__PROVIDER_KEY_NAME__);
+ public const string __EULA_RTF__ = nameof(__EULA_RTF__);
+ public const string __UPGRADECODE__ = nameof(__UPGRADECODE__);
+ public const string __VERSION__ = nameof(__VERSION__);
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs
index 4e7d29f738b..baf421d687d 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Microsoft.Deployment.WindowsInstaller;
-using Microsoft.Deployment.WindowsInstaller.Package;
+using WixToolset.Dtf.WindowsInstaller;
+using WixToolset.Dtf.WindowsInstaller.Package;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
@@ -14,6 +14,16 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
///
public class MsiUtils
{
+ ///
+ /// Query string to retrieve all the rows from the MSI Component table.
+ ///
+ private const string _getComponentsQuery = "SELECT `Component`, `ComponentId`, `Directory_`, `Attributes`, `Condition`, `KeyPath` FROM `Component`";
+
+ ///
+ /// Query string to retrieve all the rows from the MSI CustomAction table.
+ ///
+ private const string _getCustomActionsQuery = "SELECT `Action`, `Type`, `Source`, `Target` FROM `CustomAction`";
+
///
/// Query string to retrieve all the rows from the MSI File table.
///
@@ -22,12 +32,12 @@ public class MsiUtils
///
/// Query string to retrieve all the rows from the MSI Upgrade table.
///
- private const string _getUpgradeQuery = "SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes` FROM `Upgrade`";
+ private const string _getUpgradeQuery = "SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes`, `ActionProperty` FROM `Upgrade`";
///
/// Query string to retrieve the dependency provider key from the WixDependencyProvider table.
///
- private const string _getWixDependencyProviderQuery = "SELECT `ProviderKey` FROM `WixDependencyProvider`";
+ private const string _getWixDependencyProviderQuery = "SELECT `ProviderKey` FROM `Wix4DependencyProvider`";
///
/// Query string to retrieve all the rows from the MSI Directory table.
@@ -39,6 +49,25 @@ public class MsiUtils
///
private const string _getRegistryQuery = "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`";
+ ///
+ /// Gets an enumeration of all the components inside an MSI.
+ ///
+ /// The path of the MSI package to query.
+ /// And enumeration of all the components.
+ public static IEnumerable GetAllComponents(string packagePath)
+ {
+ using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly);
+ using Database db = new(packagePath, DatabaseOpenMode.ReadOnly);
+ using View componentView = db.OpenView(_getComponentsQuery);
+ List components = new();
+ componentView.Execute();
+ foreach (Record componentRecord in componentView)
+ {
+ components.Add(ComponentRow.Create(componentRecord));
+ }
+ return components;
+ }
+
///
/// Gets an enumeration of all the files inside an MSI.
///
@@ -129,6 +158,29 @@ public static IEnumerable GetRelatedProducts(string packagePath)
return Enumerable.Empty();
}
+ ///
+ /// Gets all custom actions defined in the CustomAction table of an MSI.
+ ///
+ /// The path of the MSI package to query.
+ /// An enumeration of custom action rows.
+ public static IEnumerable GetCustomActions(string packagePath)
+ {
+ using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly);
+ using Database db = new(packagePath, DatabaseOpenMode.ReadOnly);
+ if (db.Tables.Contains("CustomAction"))
+ {
+ using View customActionView = db.OpenView(_getCustomActionsQuery);
+ List customActions = new();
+ customActionView.Execute();
+ foreach (Record customAction in customActionView)
+ {
+ customActions.Add(CustomActionRow.Create(customAction));
+ }
+ return customActions;
+ }
+ return Enumerable.Empty();
+ }
+
///
/// Gets the dependency provider key from the MSI package.
///
@@ -139,7 +191,7 @@ public static string GetProviderKeyName(string packagePath)
using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly);
using Database db = new(packagePath, DatabaseOpenMode.ReadOnly);
- if (db.Tables.Contains("WixDependencyProvider"))
+ if (db.Tables.Contains("Wix4DependencyProvider"))
{
using View depProviderView = db.OpenView(_getWixDependencyProviderQuery);
depProviderView.Execute();
@@ -195,7 +247,7 @@ public static void ValidateProductVersion(Version version)
{
throw new ArgumentOutOfRangeException(string.Format(Strings.MsiProductVersionOutOfRange, nameof(version.Major), 255));
}
-
+
if (version.Minor > 255)
{
throw new ArgumentOutOfRangeException(string.Format(Strings.MsiProductVersionOutOfRange, nameof(version.Minor), 255));
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs
index e258c2ebabf..6c26459acee 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Deployment.WindowsInstaller;
+using WixToolset.Dtf.WindowsInstaller;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs
index 3619e87e3d1..2acac17cf16 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Deployment.WindowsInstaller;
+using WixToolset.Dtf.WindowsInstaller;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
@@ -55,6 +55,16 @@ public int Attributes
set;
}
+ ///
+ /// The public property to which the FindRelatedProducts action appends the installed
+ /// product's ProductCode when it's detected.
+ ///
+ public string ActionProperty
+ {
+ get;
+ set;
+ }
+
public static RelatedProduct Create(Record record)
{
string versionMin = (string)record["VersionMin"];
@@ -66,7 +76,8 @@ public static RelatedProduct Create(Record record)
VersionMin = string.IsNullOrWhiteSpace(versionMin) ? null : versionMin,
VersionMax = string.IsNullOrWhiteSpace(versionMax) ? null : versionMax,
Language = (string)record["Language"],
- Attributes = (int)record["Attributes"]
+ Attributes = (int)record["Attributes"],
+ ActionProperty = (string)record["ActionProperty"]
};
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixDocument.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixDocument.cs
new file mode 100644
index 00000000000..263fb7b64a2
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixDocument.cs
@@ -0,0 +1,155 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Class for loading and modifying existing WiX XML source files to support compositional authoring.
+ ///
+ public class WixDocument
+ {
+ private static readonly XNamespace s_wixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
+
+ private XDocument _doc;
+
+ private string _path;
+
+ public static string GetDirectoryReference()
+ => $"dir{Guid.NewGuid():N}";
+
+ public XElement Package => _doc.Root.Descendants(s_wixNamespace + "Package").FirstOrDefault();
+
+ ///
+ /// Creates a new instance of the class by loading an existing WiX XML
+ /// document from the specified file path.
+ ///
+ /// The path of the WiX XML document.
+ public WixDocument(string path)
+ {
+ _doc = XDocument.Load(path);
+ _path = path;
+ }
+
+ ///
+ /// Save the current current state of the WiX XML document to the orignal file path.
+ ///
+ public void Save()
+ {
+ _doc.Save(_path);
+ }
+
+ ///
+ /// Gets the first Directory element with matching Id attribute.
+ ///
+ /// The directory identifier to match.
+ /// The first matching Directory element or null if no elements exist.
+ public XElement GetDirectory(string id) =>
+ GetElement("Directory", id);
+
+ ///
+ /// Searches the underlying document for the first element matching the provided name and ID.
+ ///
+ /// The name of the element to find.
+ /// The Id attribute of the element to match. If null, the first matching element is returned.
+ /// Optional namespace to use. If null, the default WiX namespace is used.
+ /// The element or null if it was not found.
+ public XElement GetElement(string elementName, string id = null, XNamespace ns = null)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return _doc.Root.Descendants((ns ?? s_wixNamespace) + elementName).FirstOrDefault();
+ }
+
+ foreach (XElement element in _doc.Root.Descendants((ns ?? s_wixNamespace) + elementName))
+ {
+ if (element.Attribute("Id")?.Value == id)
+ {
+ return element;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the first Feature element with matching Id attribute.
+ ///
+ /// The feature identifier to match
+ /// The Feature element or null if it does not exist.
+ public XElement GetFeature(string id) =>
+ GetElement("Feature", id);
+
+ ///
+ /// Adds a RegistryKey element to the specified component.
+ ///
+ /// The identifier of the component.
+ /// The RegistryKey element to add.
+ ///
+ public void AddRegistryKey(string componentId, XElement registryKey)
+ {
+ var component = GetElement("Component", componentId) ??
+ throw new InvalidOperationException($"The specified component does not exist: {componentId}");
+ component.Add(registryKey);
+ }
+
+ ///
+ /// Adds a Property element to the Package.
+ ///
+ /// The property identifier.
+ ///
+ public void AddProperty(string id, string value) =>
+ Package.Add(new XElement(s_wixNamespace + "Property",
+ new XAttribute("Id", id),
+ new XAttribute("Value", value)));
+
+ ///
+ /// Adds a PropertyRef element to the Package.
+ ///
+ ///
+ public void AddPropertyRef(string id) =>
+ Package.Add(new XElement(s_wixNamespace + "PropertyRef",
+ new XAttribute("Id", id)));
+
+ ///
+ /// Adds a CustomActionRef element to the Package.
+ ///
+ ///
+ public void AddCustomActionRef(string id) =>
+ Package.Add(new XElement(s_wixNamespace + "CustomActionRef",
+ new XAttribute("Id", id)));
+
+ ///
+ /// Creates a directory element with the provided name and unique identifier.
+ ///
+ /// The name of the directory.
+ /// A new element representing the directory.
+ public static XElement CreateDirectory(string name) =>
+ CreateDirectory(name, $"dir{Guid.NewGuid():N}");
+
+ ///
+ /// Creates a directory element with the provided name and identifier.
+ ///
+ /// The name of the directory.
+ /// The identifier for the directory element. The identifier must be unique within the installer.
+ /// A new element representing the directory.
+ public static XElement CreateDirectory(string name, string id) =>
+ new XElement(s_wixNamespace + "Directory",
+ new XAttribute("Id", id),
+ new XAttribute("Name", name));
+
+ ///
+ /// Creates a RegistryKey element with the specified key path and root hive. The new element can be added as a child to any existing Component or RegistryKey element.
+ ///
+ /// The name of the registry key.
+ /// The registry key (HKLM, HKCR, etc.).
+ ///
+ public static XElement CreateRegistryKey(string key, string root = "HKLM") =>
+ new XElement(s_wixNamespace + "RegistryKey",
+ new XAttribute("Root", root),
+ new XAttribute("Key", key));
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixXmlExtensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixXmlExtensions.cs
new file mode 100644
index 00000000000..85322aee9e6
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WixXmlExtensions.cs
@@ -0,0 +1,159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ internal static class WixXmlExtensions
+ {
+ private static readonly XNamespace s_wixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
+
+ ///
+ /// Allowed parent elements for a Directory element.
+ ///
+ private static readonly string[] _directoryParentElements =
+ ["Package", "Module", "Fragment", "Directory", "DirectoryRef", "StandardDirectory"];
+
+ ///
+ /// Allowed parent elements for a ComponentGroupRef element.
+ ///
+ private static readonly string[] _componentGroupRefParentElements =
+ ["Package", "Module", "ComponentGroup", "Feature", "FeatureGroup", "FeatureRef"];
+
+ ///
+ /// Allowed parent elements for a RegistryKey element.
+ ///
+ private static readonly string[] _registryKeyParentElements = ["Component", "RegistryKey"];
+
+ ///
+ /// Allowed parent elements for a RegistryValue element.
+ ///
+ private static readonly string[] _registryValueParentElements = ["Component", "RegistryKey"];
+
+ ///
+ /// Adds a RegistryValue element to an existing element. RegistryValue elements can be added to existing
+ /// Component and RegistryKey elements.
+ ///
+ /// The parent element to which the RegistryValue will be added.
+ /// The registry value name.
+ /// The registry value.
+ /// The registry value's type.
+ /// Determines whether the registry value is the keypath of the parent component.
+ /// The parent element.
+ ///
+ public static XElement AddRegistryValue(this XElement element, string? name, string value, string type = "string", bool keyPath = false)
+ {
+ if (_registryValueParentElements.Any(e => string.Equals(e, element.Name.LocalName)))
+ {
+ var registryValue = new XElement(s_wixNamespace + "RegistryValue",
+ new XAttribute("Value", value),
+ new XAttribute("Type", type),
+ new XAttribute("KeyPath", keyPath ? "yes" : "no"));
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ registryValue.SetAttributeValue("Name", name);
+ }
+
+ element.Add(registryValue);
+ return element;
+ }
+ throw new InvalidOperationException(string.Format(Strings.InvalidChildElement, "RegistryValue", element.Name.LocalName));
+ }
+
+ ///
+ /// Adds a new RegistryKey child element to the specified parent if the parent
+ /// supports registry key elements.
+ ///
+ /// Use this method to programmatically construct WiX XML fragments that define registry
+ /// keys. The method only adds a RegistryKey element if the parent element type is valid for registry
+ /// keys.
+ /// The parent to which the RegistryKey element will be added. Must be an element
+ /// type that allows registry key children.
+ /// The registry key path to assign to the new RegistryKey element.
+ /// The root of the registry hive. Defaults to "HKLM" if not specified.
+ /// The newly created XElement representing the RegistryKey child element.
+ /// Thrown if the specified does not support adding a RegistryKey child
+ /// element.
+ public static XElement AddRegistryKey(this XElement element, string key, string? root = "HKLM")
+ {
+ if (_registryKeyParentElements.Any(e => string.Equals(e, element.Name.LocalName)))
+ {
+ var registryKey = new XElement(s_wixNamespace + "RegistryKey",
+ new XAttribute("Key", key));
+
+ // If root is null, elements can be nested and will inherit the parent's Root attribute.
+ if (!string.IsNullOrWhiteSpace(root))
+ {
+ registryKey.SetAttributeValue("Root", root);
+ }
+
+ element.Add(registryKey);
+ return registryKey;
+ }
+ throw new InvalidOperationException(string.Format(Strings.InvalidChildElement, "RegistryKey", element.Name.LocalName));
+ }
+
+ ///
+ /// Adds a new ComponentGroupRef element with the specified identifier as a child of the given XElement, if the
+ /// element supports ComponentGroupRef children.
+ ///
+ /// The parent XElement to which the ComponentGroupRef element will be added. Must be an element type that
+ /// allows ComponentGroupRef children.
+ /// The identifier to assign to the Id attribute of the new ComponentGroupRef element.
+ /// The newly created ComponentGroupRef XElement that was added as a child of the specified element.
+ /// Thrown if the specified element does not support ComponentGroupRef child elements.
+ public static XElement AddComponentGroupRef(this XElement element, string id)
+ {
+ if (_componentGroupRefParentElements.Any(e => string.Equals(e, element.Name.LocalName)))
+ {
+ var componentGroupRef = new XElement(s_wixNamespace + "ComponentGroupRef",
+ new XAttribute("Id", id));
+ element.Add(componentGroupRef);
+ return componentGroupRef;
+ }
+ throw new InvalidOperationException(string.Format(Strings.InvalidChildElement, "ComponentGroupRef", element.Name.LocalName));
+ }
+
+ ///
+ /// Adds a Directory element to an existing directory element. Directory elements can be added
+ /// to existing Directory or DirectoryRef elements to create a subdirectory.
+ ///
+ /// The identifier used when referencing the directory.
+ /// The name of the directory.
+ /// The new Directory element.
+ ///
+ public static XElement AddDirectory(this XElement element, string id, string name)
+ {
+ if (_directoryParentElements.Any(e => string.Equals(e, element.Name.LocalName)))
+ {
+ var directory = new XElement(s_wixNamespace + "Directory",
+ new XAttribute("Id", id),
+ new XAttribute("Name", name));
+
+ element.Add(directory);
+
+ return directory;
+ }
+
+ throw new InvalidOperationException(string.Format(Strings.InvalidChildElement, "Directory", element.Name.LocalName));
+ }
+
+ public static XElement AddDirectory(this XElement element, XElement directory)
+ {
+ if (_directoryParentElements.Any(e => string.Equals(e, element.Name.LocalName)))
+ {
+ element.Add(directory);
+
+ return directory;
+ }
+
+ throw new InvalidOperationException(string.Format(Strings.InvalidChildElement, "Directory", element.Name.LocalName));
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs
index 5fc3a15b9b1..c5cf0f7e1ab 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Text.Json;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
@@ -34,131 +33,92 @@ protected bool IsSxS
get;
}
- public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
- string baseIntermediateOutputPath, bool isSxS = false) :
- base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath)
+ protected override string? MsiPackageType => DefaultValues.ManifestMsi;
+
+ public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine,
+ WixToolsetConfiguration wixToolsetConfig,
+ string baseIntermediateOutputPath, bool isSxS = false, bool createWixPack = true) :
+ base(package, buildEngine, wixToolsetConfig, platform, baseIntermediateOutputPath, createWixPack)
{
Package = package;
IsSxS = isSxS;
- }
- ///
- ///
- public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null)
- {
- // Harvest the package contents before adding it to the source files we need to compile.
- string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs");
- string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data");
+ ProviderKeyName = IsSxS ? $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" :
+ $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}";
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = packageDataDirectory
- };
+ InstallationRecordKey = $@"{InstallRecordBaseKey}\InstalledManifests\{Platform}\{package.Id}\{package.PackageVersion}";
+
+ // To support upgrades, the UpgradeCode must be stable within an SDK feature band.
+ // For example, 6.0.101 and 6.0.108 must generate the same GUID for the same platform and
+ // manifest ID. The workload author must ensure the ProductVersion is higher than previously
+ // shipped versions. For SxS installs the UpgradeCode must be a random GUID.
+ UpgradeCode = IsSxS ? Guid.NewGuid() :
+ Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}");
+
+ ReplacementTokens[MsiTokens.__PROVIDER_KEY_NAME__] = ProviderKeyName;
+ ReplacementTokens[MsiTokens.__UPGRADECODE__] = UpgradeCode.ToString("B");
+ }
- if (!heat.Execute())
+ public override string Create()
+ {
+ WixDocument productDoc = CreateProduct();
+
+ // Add the manifest directories. The temporary installer in the SDK (6.0) used lower invariants
+ // of the manifest ID. We have to do the same to ensure stable GUIDs are generated for components.
+ // For example, we'll end up with authoring similar to
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ var directory = productDoc.GetDirectory(MsiDirectories.DOTNETHOME)
+ .AddDirectory(MsiDirectories.SdkManifestDir, "sdk-manifests")
+ .AddDirectory(MsiDirectories.SdkFeatureBandVersionDir, $"{Package.SdkFeatureBand}")
+ .AddDirectory(MsiDirectories.ManifestIdDir, $"{Package.ManifestId.ToLowerInvariant()}");
+
+ // For SxS installs, different manifests for the same feature band need to be installed
+ // in versioned directories.
+ if (IsSxS)
{
- throw new Exception(Strings.HeatFailedToHarvest);
+ directory.AddDirectory(MsiDirectories.ManifestVersionDir, Package.GetManifest().Version);
}
- foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f)))
+ productDoc.AddRegistryKey("C_InstallationRecord", CreateInstallationRecord());
+
+ // Harvest the package content and add the generated component group reference to an
+ // existing feature.
+ string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data");
+ string filesDirectoryId = IsSxS ?
+ MsiDirectories.ManifestVersionDir :
+ MsiDirectories.ManifestIdDir;
+ productDoc.GetFeature("F_PackageContents")
+ .AddComponentGroupRef(HarvestDirectory(packageDataDirectory, filesDirectoryId));
+
+ foreach (var file in Directory.GetFiles(packageDataDirectory))
{
NuGetPackageFiles[file] = @"\data\extractedManifest\" + Path.GetFileName(file);
}
- // Add WorkloadPackGroups.json to add to workload manifest MSI
- string? jsonContentWxs = null;
- string? jsonDirectory = null;
-
- if (WorkloadPackGroups.Any())
+ if (WorkloadPackGroups.Count > 0)
{
- jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs");
-
string jsonAsString = JsonSerializer.Serialize(WorkloadPackGroups, typeof(IList), new JsonSerializerOptions() { WriteIndented = true });
- jsonDirectory = Path.Combine(WixSourceDirectory, "json");
+ string jsonDirectory = Path.Combine(SourcePath, "json");
Directory.CreateDirectory(jsonDirectory);
string jsonFullPath = Path.GetFullPath(Path.Combine(jsonDirectory, "WorkloadPackGroups.json"));
File.WriteAllText(jsonFullPath, jsonAsString);
- HarvesterToolTask jsonHeat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory,
- OutputFile = jsonContentWxs,
- Platform = this.Platform,
- SourceDirectory = jsonDirectory,
- SourceVariableName = "JsonSourceDir",
- ComponentGroupName = "CG_PackGroupJson"
- };
-
- if (!jsonHeat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
+ productDoc.GetFeature("F_PackageContents")
+ .AddComponentGroupRef(HarvestDirectory(jsonDirectory, filesDirectoryId, "JsonSourceDir"));
NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath);
}
- CompilerToolTask candle = CreateDefaultCompiler();
- candle.AddSourceFiles(packageContentWxs,
- EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory));
-
- if (IsSxS)
- {
- candle.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version);
- }
-
- if (jsonContentWxs != null)
- {
- candle.AddSourceFiles(jsonContentWxs);
- candle.AddPreprocessorDefinition("IncludePackGroupJson", "true");
- candle.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory);
- }
- else
- {
- candle.AddPreprocessorDefinition("IncludePackGroupJson", "false");
- }
-
- // Only extract the include file as it's not compilable, but imported by various source files.
- EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
-
- // To support upgrades, the UpgradeCode must be stable within an SDK feature band.
- // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID.
- // The workload author will need to guarantee that the version for the MSI is higher than previous shipped versions
- // to ensure upgrades correctly trigger. For SxS installs we use the package identity that would include that includes
- // the package version.
- Guid upgradeCode = IsSxS ? Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.Identity};{Platform}") :
- Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}");
- string providerKeyName = IsSxS ?
- $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" :
- $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}";
-
- // Set up additional preprocessor definitions.
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledManifests");
-
- // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID.
- // We have to do the same to ensure the keypath generation produces stable GUIDs.
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}");
-
- if (!candle.Execute())
- {
- throw new Exception(Strings.FailedToCompileMsi);
- }
-
- ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions);
-
- AddDefaultPackageFiles(msi);
+ productDoc.Save();
- return msi;
+ return "";
}
public class WorkloadPackGroupJson
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs
index d0795dbb3b3..5e14ff2d6ad 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs
@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+#nullable enable
+
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
@@ -19,179 +17,70 @@ internal class WorkloadPackGroupMsi : MsiBase
///
protected override string BaseOutputName => Metadata.Id;
- public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
- string baseIntermediatOutputPath)
- : base(package.GetMsiMetadata(), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath)
+ protected override string? MsiPackageType => DefaultValues.WorkloadPackGroupMsi;
+
+ public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine,
+ WixToolsetConfiguration wixToolsetConfig,
+ string baseIntermediatOutputPath,
+ bool createWixPack = true)
+ : base(package, buildEngine, wixToolsetConfig, platform, baseIntermediatOutputPath, createWixPack)
{
_package = package;
+
+ UpgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}");
+ ProviderKeyName = $"{_package.Id},{Metadata.PackageVersion},{Platform}";
+ InstallationRecordKey = $@"{InstallRecordBaseKey}\InstalledPackGroups\{Platform}\{package.Id}\{Metadata.PackageVersion}";
+
+ ReplacementTokens[MsiTokens.__PROVIDER_KEY_NAME__] = ProviderKeyName;
+ ReplacementTokens[MsiTokens.__UPGRADECODE__] = UpgradeCode.ToString("B");
}
- public override ITaskItem Build(string outputPath, ITaskItem[] iceSuppressions)
+ public override string Create()
{
- List packageContentWxsFiles = new List();
+ WixDocument productDoc = CreateProduct();
- int packNumber = 1;
+ XElement installRecordKey = base.CreateInstallationRecord();
- MsiDirectory dotnetHomeDirectory = new MsiDirectory("dotnet", "DOTNETHOME");
- Dictionary sourceDirectoryNamesAndValues = new();
+ int packCount = 0;
foreach (var pack in _package.Packs)
{
- string packageContentWxs = Path.Combine(WixSourceDirectory, $"PackageContent.{pack.Id}.wxs");
+ // Calculate the installation directory name and ID for the pack.
+ string packDirName = WorkloadPackMsi.GetInstallDir(pack.Kind);
+ string packDirReference = WorkloadPackMsi.GetDirectoryReference(pack.Kind);
- string directoryReference;
- if (pack.Kind == WorkloadPackKind.Library)
- {
- directoryReference = dotnetHomeDirectory.GetSubdirectory("library-packs", "LibraryPacksDir").Id;
- }
- else if (pack.Kind == WorkloadPackKind.Template)
- {
- directoryReference = dotnetHomeDirectory.GetSubdirectory("template-packs", "TemplatePacksDir").Id;
- }
- else
- {
- var versionDir = dotnetHomeDirectory.GetSubdirectory("packs", "PacksDir")
- .GetSubdirectory(pack.Id, "PackDir" + packNumber)
- .GetSubdirectory($"{pack.PackageVersion}", "PackVersionDir" + packNumber);
-
- directoryReference = versionDir.Id;
- }
+ // Get the directory element associated with the pack or if it doesn't exist, add the directory and return it.
+ var packDirectory = productDoc.GetDirectory(packDirReference) ?? productDoc.GetDirectory("DOTNETHOME")
+ .AddDirectory(packDirReference, packDirName);
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
+ if (pack.Kind != WorkloadPackKind.Library && pack.Kind != WorkloadPackKind.Template)
{
- DirectoryReference = directoryReference,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = pack.DestinationDirectory,
- SourceVariableName = "SourceDir" + packNumber,
- ComponentGroupName = "CG_PackageContents" + packNumber
- };
-
- sourceDirectoryNamesAndValues[heat.SourceVariableName] = heat.SourceDirectory;
-
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
+ // Create directories for the package ID and version (which should be under the "packs" directory).
+ //
+ //
+ //
+ // Generate a new reference for the version directory that can be passed to Heat.
+ packDirReference = WixDocument.GetDirectoryReference();
+ packDirectory.AddDirectory(WixDocument.CreateDirectory(pack.Id))
+ .AddDirectory(WixDocument.CreateDirectory(pack.PackageVersion.ToString(), packDirReference));
}
- packageContentWxsFiles.Add(packageContentWxs);
-
- packNumber++;
- }
-
- // Create wxs file from dotnetHomeDirectory structure
- string directoriesWxsPath = EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory);
- var directoriesDoc = XDocument.Load(directoriesWxsPath);
- var dotnetHomeElement = directoriesDoc.Root.Descendants().Where(d => (string)d.Attribute("Id") == "DOTNETHOME").Single();
- // Remove existing subfolders of DOTNETHOME, which are for single pack MSI
- dotnetHomeElement.ReplaceWith(dotnetHomeDirectory.ToXml());
- directoriesDoc.Save(directoriesWxsPath);
-
- // Replace single ComponentGroupRef from Product.wxs with a ref for each pack
- string productWxsPath = EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory);
- var productDoc = XDocument.Load(productWxsPath);
- var ns = productDoc.Root.Name.Namespace;
- var componentGroupRefElement = productDoc.Root.Descendants(ns + "ComponentGroupRef").Single();
- componentGroupRefElement.ReplaceWith(Enumerable.Range(1, _package.Packs.Count).Select(n => new XElement(ns + "ComponentGroupRef", new XAttribute("Id", "CG_PackageContents" + n))));
- productDoc.Save(productWxsPath);
-
- // Add registry keys for packs in the pack group.
- string registryWxsPath = EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory);
- var registryDoc = XDocument.Load(registryWxsPath);
- ns = registryDoc.Root.Name.Namespace;
- var registryKeyElement = registryDoc.Root.Descendants(ns + "RegistryKey").Single();
- foreach (var pack in _package.Packs)
- {
- registryKeyElement.Add(new XElement(ns + "RegistryKey", new XAttribute("Key", pack.Id),
- new XElement(ns + "RegistryKey", new XAttribute("Key", pack.PackageVersion),
- new XElement(ns + "RegistryValue", new XAttribute("Value", ""), new XAttribute("Type", "string")))));
- }
- registryDoc.Save(registryWxsPath);
-
- CompilerToolTask candle = CreateDefaultCompiler();
-
- candle.AddSourceFiles(packageContentWxsFiles);
-
- candle.AddSourceFiles(
- EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory),
- directoriesWxsPath,
- EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory),
- productWxsPath,
- registryWxsPath);
-
- // Only extract the include file as it's not compilable, but imported by various source files.
- EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
-
- // Workload packs are not upgradable so the upgrade code is generated using the package identity as that
- // includes the package version.
- Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}");
- string providerKeyName = $"{_package.Id},{Metadata.PackageVersion},{Platform}";
+ productDoc.GetFeature("F_PackageContents")
+ .AddComponentGroupRef(HarvestDirectory(pack.DestinationDirectory, packDirReference, $"SourceDir{packCount:D4}"));
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPackGroups");
- foreach (var kvp in sourceDirectoryNamesAndValues)
- {
- candle.AddPreprocessorDefinition(kvp.Key, kvp.Value);
- }
+ // Add an install record for each pack in the pack group. Setting the root to null allows
+ // nesting RegistryKey elements.
+ installRecordKey.AddRegistryKey(pack.Id, null)
+ .AddRegistryKey(pack.PackageVersion.ToString(), null)
+ .AddRegistryValue(null, "");
- if (!candle.Execute())
- {
- throw new Exception(Strings.FailedToCompileMsi);
+ packCount++;
}
- string msiFileName = Path.Combine(outputPath, OutputName);
-
- ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions);
-
- AddDefaultPackageFiles(msi);
-
- return msi;
- }
-
- class MsiDirectory
- {
- public string Name { get; }
- public string Id { get; }
-
- public Dictionary Subdirectories { get; } = new();
-
- public MsiDirectory(string name, string id)
- {
- Name = name;
- Id = id;
- }
+ productDoc.AddRegistryKey("C_InstallationRecord", installRecordKey);
+ productDoc.Save();
- public MsiDirectory GetSubdirectory(string name, string id)
- {
- if (Subdirectories.TryGetValue(name, out var subdir))
- {
- if (!subdir.Id.Equals(id, StringComparison.Ordinal))
- {
- throw new ArgumentException($"ID {id} didn't match existing ID {subdir.Id} for directory {name}.");
- }
- return subdir;
- }
-
- subdir = new MsiDirectory(name, id);
- Subdirectories.Add(name, subdir);
- return subdir;
- }
-
- public XElement ToXml()
- {
- XNamespace ns = "http://schemas.microsoft.com/wix/2006/wi";
- var xml = new XElement(ns + "Directory");
- xml.SetAttributeValue("Id", Id);
- xml.SetAttributeValue("Name", Name);
-
- foreach (var subdir in Subdirectories.Values)
- {
- xml.Add(subdir.ToXml());
- }
-
- return xml;
- }
+ return "";
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs
index d98ae2809ca..80984747478 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs
@@ -4,7 +4,6 @@
#nullable enable
using System;
-using System.IO;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
using Microsoft.NET.Sdk.WorkloadManifestReader;
@@ -15,70 +14,53 @@ internal class WorkloadPackMsi : MsiBase
{
private WorkloadPackPackage _package;
- ///
protected override string BaseOutputName => _package.ShortName;
- public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
- string baseIntermediatOutputPath) :
- base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath)
- {
- _package = package;
- }
+ protected override string? MsiPackageType => DefaultValues.WorkloadPackMsi;
- public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null)
+ public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine,
+ WixToolsetConfiguration wixToolsetConfig,
+ string baseIntermediatOutputPath,
+ bool createWixPack = true) :
+ base(package, buildEngine, wixToolsetConfig, platform, baseIntermediatOutputPath, createWixPack)
{
- // Harvest the package contents before adding it to the source files we need to compile.
- string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs");
- string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ?
- "InstallDir" : "VersionDir";
-
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = directoryReference,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = _package.DestinationDirectory
- };
-
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
-
- CompilerToolTask candle = CreateDefaultCompiler();
-
- candle.AddSourceFiles(packageContentWxs,
- EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory));
-
- // Only extract the include file as it's not compilable, but imported by various source files.
- EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
+ _package = package;
// Workload packs are not upgradable so the upgrade code is generated using the package identity as that
// includes the package version.
- Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
- string providerKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}";
+ UpgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
+ ProviderKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}";
+ InstallationRecordKey = $@"{InstallRecordBaseKey}\InstalledPacks\{Platform}\{package.Id}\{package.PackageVersion}";
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPacks");
+ ReplacementTokens[MsiTokens.__PROVIDER_KEY_NAME__] = ProviderKeyName;
+ ReplacementTokens[MsiTokens.__UPGRADECODE__] = UpgradeCode.ToString("B");
+ }
+
+ public override string Create()
+ {
+ WixDocument productDoc = CreateProduct();
- if (!candle.Execute())
+ // Add the default installation directory based on the workload pack kind.
+ string directoryReference = MsiDirectories.InstallDir;
+ var directory = productDoc.GetDirectory(MsiDirectories.DOTNETHOME)
+ .AddDirectory(MsiDirectories.InstallDir, GetInstallDir(_package.Kind));
+
+ if (_package.Kind != WorkloadPackKind.Library && _package.Kind != WorkloadPackKind.Template)
{
- throw new Exception(Strings.FailedToCompileMsi);
+ directory.AddDirectory(MsiDirectories.PackageDir, Metadata.Id)
+ .AddDirectory(MsiDirectories.VersionDir, Metadata.PackageVersion.ToString());
+ // Override the directory reference for harvesting.
+ directoryReference = MsiDirectories.VersionDir;
}
- ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions);
+ productDoc.AddRegistryKey("C_InstallationRecord", CreateInstallationRecord());
- AddDefaultPackageFiles(msi);
+ // Harvest the template.
+ productDoc.GetFeature("F_PackageContents")
+ .AddComponentGroupRef(HarvestDirectory(_package.DestinationDirectory, directoryReference));
+ productDoc.Save();
- return msi;
+ return "";
}
///
@@ -95,6 +77,21 @@ internal static string GetInstallDir(WorkloadPackKind kind) =>
WorkloadPackKind.Tool => "tool-packs",
_ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, kind)),
};
+
+ ///
+ /// Gets the directory reference ID associated with the workload pack kind.
+ ///
+ /// The workload pack kind.
+ /// The directory reference (ID) of the installation directory.
+ internal static string GetDirectoryReference(WorkloadPackKind kind) =>
+ kind switch
+ {
+ WorkloadPackKind.Framework or WorkloadPackKind.Sdk => "PacksDir",
+ WorkloadPackKind.Library => "LibraryPacksDir",
+ WorkloadPackKind.Template => "TemplatePacksDir",
+ WorkloadPackKind.Tool => "ToolPacksDir",
+ _ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, kind)),
+ };
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackageMetadata.cs
similarity index 65%
rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs
rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackageMetadata.cs
index e34a31ef85c..1c83dd37a28 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackageMetadata.cs
@@ -8,7 +8,7 @@
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
- internal class MsiMetadata
+ internal class WorkloadPackageMetadata : IWorkloadPackageMetadata
{
public string Id
{
@@ -59,7 +59,7 @@ public string SwixPackageId
get;
}
- public MsiMetadata(string id, NuGetVersion packageVersion, Version msiVersion, string authors, string copyright, string description, string title, string? licenseUrl, string projectUrl, string swixPackageId)
+ public WorkloadPackageMetadata(string id, NuGetVersion packageVersion, Version msiVersion, string authors, string copyright, string description, string title, string? licenseUrl, string projectUrl, string swixPackageId)
{
Id = id;
PackageVersion = packageVersion;
@@ -72,22 +72,6 @@ public MsiMetadata(string id, NuGetVersion packageVersion, Version msiVersion, s
ProjectUrl = projectUrl;
SwixPackageId = swixPackageId;
}
-
- public static MsiMetadata Create(WorkloadPackageBase package)
- {
- return new(
- package.Id,
- package.PackageVersion,
- package.MsiVersion,
- package.Authors,
- package.Copyright,
- package.Description,
- package.Title,
- package.LicenseUrl,
- package.ProjectUrl,
- package.SwixPackageId
- );
- }
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs
index 28a461c2e26..defefd5d630 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs
@@ -3,12 +3,9 @@
#nullable enable
-using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text.Json;
using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
@@ -19,63 +16,40 @@ internal class WorkloadSetMsi : MsiBase
protected override string BaseOutputName => Path.GetFileNameWithoutExtension(_package.PackagePath);
- public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
- string baseIntermediatOutputPath) :
- base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath)
+ protected override string? MsiPackageType => DefaultValues.WorkloadSetMsi;
+
+ public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine,
+ WixToolsetConfiguration wixToolsetConfig,
+ string baseIntermediatOutputPath,
+ bool createWixPack = true) :
+ base(package, buildEngine, wixToolsetConfig, platform, baseIntermediatOutputPath, createWixPack)
{
_package = package;
+ InstallationRecordKey = $@"{InstallRecordBaseKey}\InstalledWorkloadSets\{Platform}\{_package.SdkFeatureBand}\{_package.PackageVersion}";
+ UpgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
+ ProviderKeyName = $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}";
+ ReplacementTokens[MsiTokens.__PROVIDER_KEY_NAME__] = ProviderKeyName;
+ ReplacementTokens[MsiTokens.__UPGRADECODE__] = UpgradeCode.ToString("B");
}
- public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions)
+ public override string Create()
{
- // Harvest the package contents before adding it to the source files we need to compile.
- string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs");
- string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data");
-
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = MsiDirectories.WorkloadSetVersionDirectory,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = packageDataDirectory
- };
-
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
+ WixDocument productDoc = CreateProduct();
- CompilerToolTask candle = CreateDefaultCompiler();
- candle.AddSourceFiles(packageContentWxs,
- EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory),
- EmbeddedTemplates.Extract("WorkloadSetProduct.wxs", WixSourceDirectory));
+ productDoc.AddRegistryKey("C_InstallationRecord", CreateInstallationRecord());
- // Extract the include file as it's not compilable, but imported by various source files.
- EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
-
- Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
- string providerKeyName = $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}";
+ var directory = productDoc.GetDirectory(MsiDirectories.DOTNETHOME)
+ .AddDirectory(MsiDirectories.SdkManifestDir, "sdk-manifests")
+ .AddDirectory(MsiDirectories.SdkFeatureBandVersionDir, $"{_package.SdkFeatureBand}")
+ .AddDirectory(MsiDirectories.WorkloadSetsDir, $"workloadsets")
+ .AddDirectory(MsiDirectories.WorkloadSetVersionDir, $"{_package.WorkloadSetVersion}");
- // Set up additional preprocessor definitions.
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.WorkloadSetVersion, $"{_package.WorkloadSetVersion}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledWorkloadSets");
-
- if (!candle.Execute())
- {
- throw new Exception(Strings.FailedToCompileMsi);
- }
-
- ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions);
-
- AddDefaultPackageFiles(msi);
+ string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data");
+ productDoc.GetFeature("F_PackageContents")
+ .AddComponentGroupRef(HarvestDirectory(packageDataDirectory, MsiDirectories.WorkloadSetVersionDir));
+ productDoc.Save();
- return msi;
+ return "";
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs
deleted file mode 100644
index 82010d2552b..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs
deleted file mode 100644
index 09ddad7ddf6..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs
deleted file mode 100644
index c20410c0c6f..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOT WIX_DOWNGRADE_DETECTED
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs
index 43b36f3ce2c..e786718d462 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs
@@ -1,26 +1,36 @@
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
+
-
-
-
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs
deleted file mode 100644
index 30bb5ead5b8..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi
deleted file mode 100644
index 0e1b15b5322..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs
deleted file mode 100644
index 01429f57ff6..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs
deleted file mode 100644
index 373441732f3..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-
-namespace Microsoft.DotNet.Build.Tasks.Workloads
-{
- ///
- /// Base class used to create projects that produce workload related artifacts.
- ///
- public abstract class ProjectTemplateBase
- {
- ///
- /// The root output directory.
- ///
- public string BaseOutputPath
- {
- get;
- set;
- }
-
- ///
- /// The root intermediate output directory.
- ///
- public string BaseIntermediateOutputPath
- {
- get;
- set;
- }
-
- ///
- /// The filename and extension of the generated project.
- ///
- protected abstract string ProjectFile
- {
- get;
- }
-
- ///
- /// The directory where the project source is generated.
- ///
- protected abstract string ProjectSourceDirectory
- {
- get;
- }
-
- protected Dictionary ReplacementTokens
- {
- get;
- } = new();
-
- ///
- /// The root directory for generated source files.
- ///
- public string SourceDirectory => Path.Combine(BaseIntermediateOutputPath, "src");
-
- public ProjectTemplateBase(string baseIntermediateOutputPath, string baseOutputPath)
- {
- BaseIntermediateOutputPath = baseIntermediateOutputPath;
- BaseOutputPath = baseOutputPath;
- }
-
- ///
- /// Generates the project template and returns the path to the project file.
- ///
- /// The path to the project file.
- public abstract string Create();
- }
-}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs
index 471219c020c..984101e0bcd 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
@@ -133,7 +133,7 @@ internal static string ComponentTitleCannotBeNull {
}
///
- /// Looks up a localized string similar to Failed to compile MSI. .
+ /// Looks up a localized string similar to Failed to compile MSI, command line: {0}.
///
internal static string FailedToCompileMsi {
get {
@@ -159,6 +159,15 @@ internal static string HeatFailedToHarvest {
}
}
+ ///
+ /// Looks up a localized string similar to {0} is not a valid child element of {1}..
+ ///
+ internal static string InvalidChildElement {
+ get {
+ return ResourceManager.GetString("InvalidChildElement", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Invalid workload set package: {0}. The package does not contain a "data" directory..
///
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx
index 9ed0343354d..61f7abcea1a 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx
@@ -1,4 +1,4 @@
-
+