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 a093ae1c0a5..b678ca205ac 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -95,8 +95,13 @@
-
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs
index 670aa8a5659..e5e64f77ac3 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,100 @@ 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,
+ OverridePackageVersions = true,
WixToolsetPath = WixToolsetPath,
WorkloadSetPackageFiles = workloadSetPackages
};
- 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);
+
+ // 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);
// Workload sets are SxS. Verify that we don't have an Upgrade table.
- Assert.False(MsiUtils.HasTable(msi.ItemSpec, "Upgrade"));
+ // This requires suppressing the default behavior by setting Package@UpgradeStrategy to "none".
+ MsiUtils.HasTable(arm64MsiPath, "Upgrade").Should().BeFalse("because workload sets are side-by-side");
// 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 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.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\x64")).FirstOrDefault();
+ ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\arm64")).FirstOrDefault();
Assert.Equal(DefaultValues.PackageTypeMsiWorkloadSet, workloadSetSwixItem.GetMetadata(Metadata.PackageType));
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.DoesNotContain("vs.package.chip=arm64", msiSwr);
+ Assert.Contains("vs.package.machineArch=arm64", 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..635ce486ef7 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs
@@ -2,14 +2,12 @@
// 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 FluentAssertions;
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 Xunit;
@@ -18,240 +16,57 @@ 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 static 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 baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLPG");
if (Directory.Exists(baseIntermediateOutputPath))
{
Directory.Delete(baseIntermediateOutputPath, recursive: true);
}
- 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")
- };
-
- ITaskItem[] componentResources = new[]
- {
- 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[]
- {
- 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")
+ 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")
};
IBuildEngine buildEngine = new MockBuildEngine();
-
CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload()
{
AllowMissingPacks = true,
BaseOutputPath = TestBase.BaseOutputPath,
BaseIntermediateOutputPath = baseIntermediateOutputPath,
BuildEngine = buildEngine,
- ComponentResources = componentResources,
+ ComponentResources = Array.Empty(),
+ CreateWorkloadPackGroups = true,
+ DisableParallelPackageGroupProcessing = false,
+ IsOutOfSupportInVisualStudio = false,
ManifestMsiVersion = null,
- PackageSource = TestBase.TestAssetsPath,
- ShortNames = shortNames,
- WixToolsetPath = TestBase.WixToolsetPath,
- WorkloadManifestPackageFiles = manifestsPackages,
- IsOutOfSupportInVisualStudio = true
+ PackageSource = packageSource,
+ ShortNames = Array.Empty(),
+ WixToolsetPath = WixToolsetPath,
+ WorkloadManifestPackageFiles = manifestsPackages
};
bool result = createWorkloadTask.Execute();
-
Assert.True(result);
- ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-x64.msi")).FirstOrDefault();
- Assert.NotNull(manifestMsiItem);
- // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation.
- // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band,
- Assert.Equal("{C4F269D9-6B65-36C5-9556-75B78EFE9EDA}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode));
- // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would
- // be generated.
- Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion));
- Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,x64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec));
-
- // Process the template in the summary information stream. This is the only way to verify the intended platform
- // of the MSI itself.
- using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false);
- Assert.Equal("x64;1033", si.Template);
-
- // Verify the SWIX authoring for the component representing the workload in VS. The first should be a standard
- // component. There should also be a second preview component.
+ // 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("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
- Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
- string previewComponentSwr = File.ReadAllText(
- Path.Combine(Path.GetDirectoryName(
- createWorkloadTask.SwixProjects.FirstOrDefault(
- i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr"));
- Assert.Contains("package name=microsoft.net.sdk.emscripten.pre", previewComponentSwr);
-
- // Emscripten is an abstract workload so it should be a component group.
- Assert.Contains("vs.package.type=component", componentSwr);
- Assert.Contains("vs.package.outOfSupport=yes", componentSwr);
- Assert.Contains("isUiGroup=yes", componentSwr);
- Assert.Contains("version=5.6.7.8", componentSwr);
-
- Assert.Contains("vs.package.type=component", previewComponentSwr);
- Assert.Contains("isUiGroup=yes", previewComponentSwr);
- Assert.Contains("version=5.6.7.8", previewComponentSwr);
-
- // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased
- // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the
- // aliased workload pack packages.
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr);
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr);
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr);
-
- // Pack dependencies for preview components should be identical to the non-preview component.
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", previewComponentSwr);
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", previewComponentSwr);
- 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"));
- 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.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"));
-
- // 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();
- 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);
-
- // 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));
- Assert.All(swixComponentProjects, c => Assert.True(c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "true" ||
- !c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "false"));
- }
-
- [WindowsOnlyFact]
- public static 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);
- }
-
- ITaskItem[] manifestsPackages = new[]
- {
- 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[]
- {
- 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[]
- {
- 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,
- BaseIntermediateOutputPath = baseIntermediateOutputPath,
- BuildEngine = buildEngine,
- ComponentResources = componentResources,
- ManifestMsiVersion = null,
- PackageSource = TestBase.TestAssetsPath,
- ShortNames = shortNames,
- WixToolsetPath = TestBase.WixToolsetPath,
- WorkloadManifestPackageFiles = manifestsPackages,
- };
-
- bool result = createWorkloadTask.Execute();
-
- Assert.True(result);
- ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-arm64.msi")).FirstOrDefault();
- Assert.NotNull(manifestMsiItem);
-
- // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation.
- // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band,
- Assert.Equal("{CBA7CF4A-F3C9-3B75-8F1F-0D08AF7CD7BE}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode));
- // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would
- // be generated.
- Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion));
- Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,arm64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec));
-
- // Process the template in the summary information stream. This is the only way to verify the intended platform
- // of the MSI itself.
- using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false);
- Assert.Equal("Arm64;1033", si.Template);
-
- // Verify the SWIX authoring for the component representing the workload in VS.
- string componentSwr = File.ReadAllText(
- Path.Combine(Path.GetDirectoryName(
- createWorkloadTask.SwixProjects.FirstOrDefault(
- i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
- Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
-
- // Emscripten is an abstract workload so it should be a component group.
- Assert.Contains("vs.package.type=component", componentSwr);
- Assert.Contains("isUiGroup=yes", componentSwr);
- Assert.Contains("version=5.6.7.8", componentSwr);
- // Default setting should be off
- Assert.Contains("vs.package.outOfSupport=no", componentSwr);
-
- // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased
- // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the
- // aliased workload pack packages.
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr);
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr);
- Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr);
+ i => i.ItemSpec.Contains("wasm.tools.10.0.swixproj")).ItemSpec), "component.swr"));
+ Assert.Contains("vs.dependency id=wasm.tools.WorkloadPacks", 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"));
- 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);
+ // Manifest installers should contain additional JSON files describing pack groups.
+ ITaskItem manifestMsi = createWorkloadTask.Msis.First(m => m.GetMetadata(Metadata.PackageType) == DefaultValues.ManifestMsi);
+ MsiUtils.GetAllFiles(manifestMsi.ItemSpec).Should().Contain(f => f.FileName.EndsWith("WorkloadPackGroups.json"));
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs
new file mode 100644
index 00000000000..6fb208ee4b0
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs
@@ -0,0 +1,255 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Arcade.Test.Common;
+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
+{
+ [Collection("Emscripten")]
+ public class EmscriptenTests : TestBase
+ {
+ [WindowsOnlyFact]
+ public static 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 baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WL");
+
+ if (Directory.Exists(baseIntermediateOutputPath))
+ {
+ Directory.Delete(baseIntermediateOutputPath, recursive: true);
+ }
+
+ 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 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 = BaseOutputPath,
+ BaseIntermediateOutputPath = baseIntermediateOutputPath,
+ BuildEngine = buildEngine,
+ ComponentResources = componentResources,
+ ManifestMsiVersion = null,
+ PackageSource = TestAssetsPath,
+ ShortNames = shortNames,
+ WixToolsetPath = WixToolsetPath,
+ WorkloadManifestPackageFiles = manifestsPackages,
+ IsOutOfSupportInVisualStudio = true
+ };
+
+ bool result = createWorkloadTask.Execute();
+
+ Assert.True(result);
+ ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-x64.msi")).FirstOrDefault();
+ Assert.NotNull(manifestMsiItem);
+
+ // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation.
+ // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band,
+ Assert.Equal("{C4F269D9-6B65-36C5-9556-75B78EFE9EDA}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode));
+ // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would
+ // be generated.
+ Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion));
+ Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,x64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec));
+
+ // Process the template in the summary information stream. This is the only way to verify the intended platform
+ // of the MSI itself.
+ using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false);
+ Assert.Equal("x64;1033", si.Template);
+
+ // Verify the SWIX authoring for the component representing the workload in VS. The first should be a standard
+ // component. There should also be a second preview component.
+ string componentSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
+ Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
+ string previewComponentSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr"));
+ Assert.Contains("package name=microsoft.net.sdk.emscripten.pre", previewComponentSwr);
+
+ // Emscripten is an abstract workload so it should be a component group.
+ Assert.Contains("vs.package.type=component", componentSwr);
+ Assert.Contains("vs.package.outOfSupport=yes", componentSwr);
+ Assert.Contains("isUiGroup=yes", componentSwr);
+ Assert.Contains("version=5.6.7.8", componentSwr);
+
+ Assert.Contains("vs.package.type=component", previewComponentSwr);
+ Assert.Contains("isUiGroup=yes", previewComponentSwr);
+ Assert.Contains("version=5.6.7.8", previewComponentSwr);
+
+ // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased
+ // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the
+ // aliased workload pack packages.
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr);
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr);
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr);
+
+ // Pack dependencies for preview components should be identical to the non-preview component.
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", previewComponentSwr);
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", previewComponentSwr);
+ 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"));
+ 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.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"));
+
+ // 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();
+ 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);
+
+ // 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));
+ Assert.All(swixComponentProjects, c => Assert.True(c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "true" ||
+ !c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "false"));
+ }
+
+ [WindowsOnlyFact]
+ public static 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);
+ }
+
+ 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 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 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 = BaseOutputPath,
+ BaseIntermediateOutputPath = baseIntermediateOutputPath,
+ BuildEngine = buildEngine,
+ ComponentResources = componentResources,
+ ManifestMsiVersion = null,
+ PackageSource = TestAssetsPath,
+ ShortNames = shortNames,
+ WixToolsetPath = WixToolsetPath,
+ WorkloadManifestPackageFiles = manifestsPackages,
+ };
+
+ bool result = createWorkloadTask.Execute();
+
+ Assert.True(result);
+ ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-arm64.msi")).FirstOrDefault();
+ Assert.NotNull(manifestMsiItem);
+
+ // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation.
+ // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band,
+ Assert.Equal("{CBA7CF4A-F3C9-3B75-8F1F-0D08AF7CD7BE}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode));
+ // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would
+ // be generated.
+ Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion));
+ Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,arm64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec));
+
+ // Process the template in the summary information stream. This is the only way to verify the intended platform
+ // of the MSI itself.
+ using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false);
+ Assert.Equal("Arm64;1033", si.Template);
+
+ // Verify the SWIX authoring for the component representing the workload in VS.
+ string componentSwr = File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(
+ createWorkloadTask.SwixProjects.FirstOrDefault(
+ i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
+ Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
+
+ // Emscripten is an abstract workload so it should be a component group.
+ Assert.Contains("vs.package.type=component", componentSwr);
+ Assert.Contains("isUiGroup=yes", componentSwr);
+ Assert.Contains("version=5.6.7.8", componentSwr);
+ // Default setting should be off
+ Assert.Contains("vs.package.outOfSupport=no", componentSwr);
+
+ // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased
+ // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the
+ // aliased workload pack packages.
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr);
+ Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr);
+ 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"));
+ 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);
+ }
+ }
+}
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 c86b28bae03..b572b0bb911 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
@@ -13,6 +13,12 @@
+
+
+
+
+
+
@@ -23,26 +29,68 @@
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
@@ -55,6 +103,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -62,14 +146,5 @@
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
index 05a2099959d..d3770320264 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs
@@ -2,55 +2,109 @@
// 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 FluentAssertions;
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 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")]
+ [Collection("MSI tests")]
public class MsiTests : TestBase
{
- private static ITaskItem BuildManifestMsi(string path, string msiVersion = "1.2.3", string platform = "x64", string msiOutputPath = null)
+ ///
+ /// Helper method for generating workload manifest MSIs.
+ ///
+ /// The directory to use for generated output (WiX project, MSI, etc.)
+ /// The path of the workload manifest NuGet package.
+ /// The ProductVersion to assign to the MSI.
+ /// The platform of 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 outputDirectory, string packagePath, string msiVersion = "1.2.3", string platform = "x64",
+ bool allowSideBySideInstalls = true, bool generateWixpack = false, string wixpackOutputDirectory = null)
{
- TaskItem packageItem = new(path);
- WorkloadManifestPackage pkg = new(packageItem, PackageRootDirectory, new Version(msiVersion));
+ Directory.CreateDirectory(outputDirectory);
+ File.Copy(Path.Combine(TestAssetsPath, "NuGet.config"), Path.Combine(outputDirectory, "NuGet.config"), overwrite: true);
+ TaskItem packageItem = new(packagePath);
+ WorkloadManifestPackage pkg = new(packageItem, Path.Combine(outputDirectory, "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);
+ WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), outputDirectory,
+ allowSideBySideInstalls, overridePackageVersions: true, generateWixpack: generateWixpack,
+ wixpackOutputDirectory: wixpackOutputDirectory);
+
+ return msi.Build(Path.Combine(outputDirectory, "msi"));
}
[WindowsOnlyFact]
- public void WorkloadManifestsIncludeInstallationRecords()
+ public void ItCanBuildWorkloadSdkPackMsi()
{
- 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);
+ string testCaseDirectory = GetTestCaseDirectory();
+ string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg");
+ string msiOutputDirectory = Path.Combine(testCaseDirectory, "msi");
+
+ TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg"));
+ WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, 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, packageContentsDirectory, null, null);
+ workloadPackPackage.Extract();
+ var workloadPackMsi = new WorkloadPackMsi(workloadPackPackage, "x64", new MockBuildEngine(),
+ WixToolsetPath, testCaseDirectory, overridePackageVersions: true);
+
+ // Build the MSI and verify its contents
+ var msi = workloadPackMsi.Build(msiOutputDirectory);
+ string msiPath = msi.GetMetadata(Metadata.FullPath);
+
+ // Process the summary information stream's template to extract the MSIs target platform.
+ using SummaryInfo si = new(msiPath, enableWrite: false);
+ Assert.Equal("x64;1033", si.Template);
+
+ // 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";
- 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"
- );
+ 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 ItCanBuildSideBySideManifestMsis()
{
- string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg");
+ string outputDirectory = GetTestCaseDirectory();
// 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(outputDirectory, 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(outputDirectory, 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
@@ -70,55 +124,71 @@ public void ItCanBuildSideBySideManifestMsis()
d.Directory == "ManifestVersionDir" &&
d.DirectoryParent == "ManifestIdDir" &&
d.DefaultDir.EndsWith("|6.0.4"));
-
- // 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));
}
[WindowsOnlyFact]
public void ItCanBuildAManifestMsi()
{
- string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg");
- 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);
+ string outputDirectory = GetTestCaseDirectory();
+ string wixpackOutputDirectory = Path.Combine(outputDirectory, "wixpack");
- ITaskItem item = msi.Build(MsiOutputPath);
+ ITaskItem msi = BuildManifestMsi(outputDirectory,
+ Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"),
+ allowSideBySideInstalls: false,
+ generateWixpack: true,
+ wixpackOutputDirectory: wixpackOutputDirectory);
- string msiPath = item.GetMetadata(Metadata.FullPath);
+ string msiPath = msi.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));
+ // UpgradeCode is predictable/stable for manifest MSIs that support major upgrades.
+ string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode);
+ Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", 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");
+ MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir",
+ "because the manifest MSI supports major upgrades");
+
+ // 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\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3";
+ string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Mono.ToolChain,6.0.200,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.Workload.Mono.ToolChain,6.0.200,x64",
+ expectedProductCode, upgradeCode, "1.2.3");
+ ValidateDependencyProviderKey(registryKeys, dependencyProviderKey);
+
+ // 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"));
+
+ // Verify that the wixpack archive was created.
+ Assert.True(File.Exists(msi.GetMetadata(Metadata.Wixpack)));
}
[WindowsOnlyFact]
public void ItCanBuildATemplatePackMsi()
{
- string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg");
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);
+ string outputDirectory = GetTestCaseDirectory();
+ string pkgDirectory = Path.Combine(outputDirectory, "pkg");
+ string msiDirectory = Path.Combine(outputDirectory, "msi");
+ WorkloadPack templatePack = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null);
+ TemplatePackPackage pkg = new(templatePack, packagePath, ["x64"], pkgDirectory);
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, WixToolsetPath, outputDirectory, overridePackageVersions: true);
+ ITaskItem item = msi.Build(msiDirectory);
string msiPath = item.GetMetadata(Metadata.FullPath);
@@ -126,7 +196,8 @@ public void ItCanBuildATemplatePackMsi()
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));
@@ -135,11 +206,115 @@ public void ItCanBuildATemplatePackMsi()
// Template packs should pull in the raw nupkg. We can verify by query 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);
+
+ var directories = MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory);
+ directories.Should().NotContain("PackageDir", "because it's a template pack");
+ directories.Should().Contain("InstallDir", "because it's a workload pack");
+
+ // 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);
+ }
+
+ [WindowsOnlyFact]
+ public void ItCanBuildAWorkPackGroupMsi()
+ {
+ string outputDirectory = GetTestCaseDirectory();
+ string packageContentsDirectory = Path.Combine(outputDirectory, "pkg");
+ string msiOutputDirectory = Path.Combine(outputDirectory, "msi");
+ string pkgOutputDirectory = Path.Combine(outputDirectory, "nuget");
+ 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, packageContentsDirectory, 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();
+ 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"],
+ packageContentsDirectory, null, null));
+ }
+ }
+
+ var groupPackage = new WorkloadPackGroupPackage(workload.Id);
+ groupPackage.Packs.AddRange(workloadPackPackages);
+ groupPackage.ManifestsPerPlatform["x64"] = new([manifestPackage]);
+
+ var buildEngine = new MockBuildEngine();
+
+ foreach (var p in workloadPackPackages)
+ {
+ p.Extract();
+ }
+
+ WorkloadPackGroupMsi msi = new(groupPackage, "x64", buildEngine, outputDirectory, overridePackageVersions: true);
+ ITaskItem msiWorkloadPackGroupOutputItem = msi.Build(msiOutputDirectory);
+ string msiPath = msiWorkloadPackGroupOutputItem.GetMetadata(Metadata.FullPath);
+
+ MsiPayloadPackageProject csproj = new(msi.Metadata, msiWorkloadPackGroupOutputItem, outputDirectory, pkgOutputDirectory, msi.NuGetPackageFiles);
+ msiWorkloadPackGroupOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
+
+ // Build individual pack MSIs to compare against the pack group.
+ var sdkPackPackage = workloadPackPackages.FirstOrDefault(p => p.Id == "Microsoft.NET.Runtime.WebAssembly.Sdk");
+ WorkloadPackMsi sdkPackMsi = new(sdkPackPackage, "x64", buildEngine, WixToolsetPath, outputDirectory, overridePackageVersions: true);
+ ITaskItem sdkPackMsiItem = sdkPackMsi.Build(msiOutputDirectory);
+ string sdkPackMsiPath = sdkPackMsiItem.GetMetadata(Metadata.FullPath);
+
+ // Verify workdload record keys for the pack group.
+ MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r =>
+ r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Runtime.WebAssembly.Sdk\10.0.0");
+ MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r =>
+ r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Sdk.WebAssembly.Pack\10.0.0");
+ MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r =>
+ r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.Mono.browser-wasm\10.0.0");
+ MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r =>
+ r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.AOT.win-x64.Cross.browser-wasm\10.0.0");
+
+ // Verify pack directories
+ MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("PacksDir", "because the pack group contains SDK packs");
+ MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("LibraryPacksDir", "because the pack group contains a library pack");
- // 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));
+ // Individual pack MSIs and pack group should have stable IDs for their components.
+ // Pick a unique file from the File table, then locate the matching component in the pack
+ // MSI and verify that the pack group MSI contains a component with the same ID.
+ FileRow f1 = MsiUtils.GetAllFiles(sdkPackMsiPath).First(f => f.FileName.EndsWith("Sdk.props"));
+ ComponentRow c1 = MsiUtils.GetAllComponents(sdkPackMsiPath).First(c => c.Component == f1.Component_);
+ MsiUtils.GetAllComponents(msiPath).Should().Contain(c => c.ComponentId == c1.ComponentId,
+ "Packs and PackGroups should share components");
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
index 99b98e1b112..955047d7ca7 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
@@ -14,7 +14,7 @@
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
{
- [Collection("SWIX Package")]
+ [Collection("SWIX Package Generation")]
public class SwixPackageTests : TestBase
{
[WindowsOnlyFact]
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
index 52fb73d78bf..278027c4887 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs
@@ -2,20 +2,81 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.IO;
+using Microsoft.Arcade.Test.Common;
+using Microsoft.Build.Utilities;
+using Microsoft.DotNet.Build.Tasks.Workloads.Msi;
+using FluentAssertions;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
{
public abstract class TestBase
{
+ ///
+ /// This is a version of Arcade that contains updated tasks for creating WiX packs that support
+ /// signing MSIs built using WiX v5.
+ ///
+ public static readonly string MicrosoftDotNetBuildTasksInstallersPackageVersion = "10.0.0-beta.25420.109";
+
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 PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg");
+
+ public static readonly string TestOutputRoot = Path.Combine(AppContext.BaseDirectory, "TEST_OUTPUT");
+
+ ///
+ /// Returns a new, random directory for a test case.
+ ///
+ public string GetTestCaseDirectory() =>
+ Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+
+ protected static void ValidateInstallationRecord(IEnumerable registryKeys,
+ string installationRecordKey, string expectedProviderKey, string expectedProductCode, string expectedUpgradeCode,
+ string expectedProductVersion,
+ string expectedProductLanguage = "#1033")
+ {
+ registryKeys.Should().Contain(r => r.Key == installationRecordKey &&
+ r.Root == 2 &&
+ r.Name == "DependencyProviderKey" &&
+ r.Value == expectedProviderKey);
+ registryKeys.Should().Contain(r => r.Key == installationRecordKey &&
+ r.Root == 2 &&
+ r.Name == "ProductCode" &&
+ string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase));
+ registryKeys.Should().Contain(r => r.Key == installationRecordKey &&
+ r.Root == 2 &&
+ r.Name == "UpgradeCode" &&
+ string.Equals(r.Value, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase));
+ registryKeys.Should().Contain(r => r.Key == installationRecordKey &&
+ r.Root == 2 &&
+ r.Name == "ProductVersion" &&
+ r.Value == expectedProductVersion);
+ registryKeys.Should().Contain(r => r.Key == installationRecordKey &&
+ r.Root == 2 &&
+ r.Name == "ProductLanguage" &&
+ r.Value == expectedProductLanguage);
+ }
+
+ protected static void ValidateDependencyProviderKey(IEnumerable registryKeys, string dependencyProviderKey)
+ {
+ // Dependency provider entries references the ProductVersion and ProductName properties. These
+ // properties are set by the installer service at install time.
+ registryKeys.Should().Contain(r => r.Key == dependencyProviderKey &&
+ r.Root == -1 &&
+ r.Name == "Version" &&
+ r.Value == "[ProductVersion]");
+ registryKeys.Should().Contain(r => r.Key == dependencyProviderKey &&
+ r.Root == -1 &&
+ r.Name == "DisplayName" &&
+ r.Value == "[ProductName]");
+ }
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs
new file mode 100644
index 00000000000..64f556cd535
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs
@@ -0,0 +1,109 @@
+// 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;
+using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
+using Xunit;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests
+{
+ public class WixProjectTests : TestBase
+ {
+ [WindowsOnlyFact]
+ public void ItGeneratesAnSdkStyleProject()
+ {
+ var wixproj = new WixProject("5.0.2");
+ string projectDir = GetTestCaseDirectory();
+ string wixProjPath = Path.Combine(projectDir, "msi.wixproj");
+ Directory.CreateDirectory(projectDir);
+
+ wixproj.Save(wixProjPath);
+
+ string projectContents = File.ReadAllText(wixProjPath);
+
+ Assert.StartsWith(@"")]
+ [InlineData("Microsoft.WixToolset.Heat", "5.0.3", false, @"")]
+ [InlineData("Microsoft.WixToolset.Heat", "5.0.3", true, @"")]
+ public void PackageReferencesCanBeAdded(string packageId, string packageVersion, bool overridePackageVersions,
+ string expectedPackageReference)
+ {
+ var wixproj = new WixProject("5.0.2")
+ {
+ OverridePackageVersions = overridePackageVersions
+ };
+
+ string projectDir = GetTestCaseDirectory();
+ string wixProjPath = Path.Combine(projectDir, "msi.wixproj");
+ Directory.CreateDirectory(projectDir);
+
+ wixproj.AddPackageReference(packageId, packageVersion);
+ wixproj.Save(wixProjPath);
+
+ string projectContents = File.ReadAllText(wixProjPath);
+
+ Assert.Contains("Microsoft.WixToolset.Sdk/5.0.2", projectContents);
+ Assert.Contains(expectedPackageReference, projectContents);
+ }
+
+ [WindowsOnlyFact]
+ public void PreprocessorDefinitionsCanBeAdded()
+ {
+ var wixproj = new WixProject("5.0.2");
+ string projectDir = GetTestCaseDirectory();
+ string wixProjPath = Path.Combine(projectDir, "msi.wixproj");
+ Directory.CreateDirectory(projectDir);
+
+ wixproj.AddPreprocessorDefinition("Foo", " Bar ");
+ wixproj.Save(wixProjPath);
+
+ string projectContents = File.ReadAllText(wixProjPath);
+
+ Assert.Contains("$(DefineConstants);Foo= Bar x64", projectContents,StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config
new file mode 100644
index 00000000000..a20efd04673
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..cf4a6554f31 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;
}
+ ///
+ /// Aggregates multiple packs 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,12 +166,13 @@ protected override bool ExecuteCore()
Dictionary manifestMsisByPlatform = new();
foreach (string platform in SupportedPlatforms)
{
- var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, EnableSideBySideManifests);
+ var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, BaseIntermediateOutputPath,
+ EnableSideBySideManifests, generateWixpack: GenerateWixPack);
manifestMsisToBuild.Add(manifestMsi);
manifestMsisByPlatform[platform] = manifestMsi;
}
- // If we're supporting SxS manifests, generate a package group to wrap the manifest VS packages
+ // If we're supporting SxS manifests, a package group to wrap the manifest VS packages
// so we don't deal with unstable package IDs during VS insertions.
if (EnableSideBySideManifests)
{
@@ -285,7 +286,7 @@ protected override bool ExecuteCore()
{
string platform = kvp.Key;
- // The key is the paths to the packages included in the pack group, sorted in alphabetical order
+ // The key is the path to the packages included in the pack group, sorted in alphabetical order
string uniquePackGroupKey = string.Join("\r\n", kvp.Value.Select(p => p.PackagePath).OrderBy(p => p));
if (!packGroupPackages.TryGetValue(uniquePackGroupKey, out var groupPackage))
{
@@ -329,79 +330,88 @@ protected override bool ExecuteCore()
}
}
+ // Depulicate packages and extract them. Building and extrating 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);
-
- // 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());
+ // Extract the contents of the workload pack package.
+ Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath));
+ data.Package.Extract();
- 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, WixToolsetPath, BaseIntermediateOutputPath,
+ generateWixPack: GenerateWixPack);
+ 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");
+
+ 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, BaseIntermediateOutputPath,
+ generateWixPack: GenerateWixPack);
+ 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)
- {
+ // Always generate pack groups for VS.
PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage =>
{
// Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch
@@ -424,14 +434,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))
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..20430ef46fe 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,8 @@ protected override bool ExecuteCore()
foreach (string platform in SupportedPlatforms)
{
var workloadSetMsi = new WorkloadSetMsi(workloadSetPackage, platform, BuildEngine,
- WixToolsetPath, BaseIntermediateOutputPath);
+ BaseIntermediateOutputPath, overridePackageVersions: OverridePackageVersions,
+ generateWixPack: GenerateWixPack);
workloadSetMsisToBuild.Add(workloadSetMsi);
}
@@ -67,7 +68,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);
@@ -80,7 +81,8 @@ protected override bool ExecuteCore()
// Generate a .swixproj for packaging the MSI in Visual Studio. We'll default to using machineArch always. Workload sets
// are being introduced in .NET 8 and the corresponding versions of VS all support the machineArch property.
- MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, workloadSetPackage.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform));
+ MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, workloadSetPackage.SdkFeatureBand,
+ chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform));
string swixProj = swixProject.Create();
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
index 7fd2709c466..513978cc7b2 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
@@ -8,6 +8,20 @@ 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;
+
+ ///
+ /// Default component group identifier used when harvesting a directory.
+ ///
+ public const string DefaultComponentGroupName = "CG_PackageContents";
+
///
/// Prefix used in Visual Studio for SWIX based package group.
///
@@ -22,7 +36,12 @@ internal static class DefaultValues
///
/// The default value to assign to the Manufacturer property of an MSI.
///
- public static readonly string Manufacturer = "Microsoft Corporation";
+ public const string Manufacturer = "Microsoft Corporation";
+
+ ///
+ /// Default Feature ID to reference when harvesting files.
+ ///
+ public const string PackageContentsFeatureId = "F_PackageContents";
public static readonly string x86 = "x64";
public static readonly string x64 = "x64";
@@ -58,5 +77,25 @@ 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";
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
index 61c5f9f553c..524bb730bc5 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs
@@ -69,12 +69,16 @@ static EmbeddedTemplates()
{
{ "DependencyProvider.wxs", $"{ns}.MsiTemplate.DependencyProvider.wxs" },
{ "Directories.wxs", $"{ns}.MsiTemplate.Directories.wxs" },
+ { "WorkloadPackDirectories.wxs", $"{ns}.MsiTemplate.WorkloadPackDirectories.wxs" },
{ "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" },
+ { "Files.wxs",$"{ns}.MsiTemplate.Files.wxs" },
{ "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" },
{ "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" },
+ { "PackDirectories.wxs", $"{ns}.MsiTemplate.PackDirectories.wxs" },
+ { "DirectoryReference.wxs", $"{ns}.MsiTemplate.DirectoryReference.wxs" },
{ "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" },
{ "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" },
- { "Variables.wxi", $"{ns}.MsiTemplate.Variables.wxi" },
+ { "Directory.Build.targets", $"{ns}.MsiTemplate.Directory.Build.targets.pp" },
{ $"msi.swr", $"{ns}.SwixTemplate.msi.swr" },
{ $"msi.swixproj", $"{ns}.SwixTemplate.msi.swixproj" },
@@ -83,7 +87,7 @@ static EmbeddedTemplates()
{ $"component.swixproj", $"{ns}.SwixTemplate.component.swixproj" },
{ $"manifest.vsmanproj", $"{ns}.SwixTempalte.manifest.vsmanproj" },
{ $"packageGroup.swr", $"{ns}.SwixTemplate.packageGroup.swr" },
- { $"packageGroup.swixproj", $"{ns}.SwixTemplate.packageGroup.swixproj" },
+ { $"packageGroup.swixproj", $"{ns}.SwixTemplate.packageGroup.swixproj" },
{ "Icon.png", $"{ns}.Misc.Icon.png" },
{ "LICENSE.TXT", $"{ns}.Misc.LICENSE.TXT" },
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
index 229bb00056e..37b159bd01c 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs
@@ -58,5 +58,10 @@ internal static class Metadata
/// the compiler.
///
public static readonly string WixObj = nameof(WixObj);
+
+ ///
+ /// Metadata containing the full path to the generated wixpack archive.
+ ///
+ public static readonly string Wixpack = nameof(Wixpack);
}
}
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 c41a6f4b02b..454baf8fc53 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 @@
-
+
$(BundledNETCoreAppTargetFramework);$(NetFrameworkToolCurrent)
@@ -38,11 +38,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -57,20 +57,30 @@
+
+
-
-
+
+
-
+
+
+
+
+ $([System.IO.File]::ReadAllText(ToolsetInfo.cs.pp))
+ $(ToolsetInfoText.Replace('{MicrosoftWixToolsetSdkVersion}', $(MicrosoftWixToolsetSdkVersion)))
+ $(ToolsetInfoText.Replace('{ArcadeVersion}', $(VersionPrefix)-*))
+
+
+
+
+
+
+
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..3ca0fd7dc8c
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs
@@ -0,0 +1,78 @@
+// 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 Guid ComponentId
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// External key of an entry in the Directory table.
+ ///
+ public string Directory_
+ {
+ get;
+ set;
+ }
+
+ public int Attributes
+ {
+ get;
+ set;
+ }
+
+ public string Condition
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// The key path for 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 = Guid.Parse(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/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/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs
index 0e5c1df333c..20e9d415980 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,39 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
+using System.Text;
+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
{
+ ///
+ /// Base class used for building MSIs.
+ ///
internal abstract class MsiBase
{
+ ///
+ /// Used to track the number of directories created.
+ ///
+ private int _dirCount = 0;
+
+ ///
+ /// Used to track Files elements added for harvesting.
+ ///
+ private int _filesCount = 0;
+
+ ///
+ /// The Arcade package that contains the CreateWixBuildWixpack task to support signing.
+ ///
+ private const string _MicrosoftDotNetBuildTaskInstallers = "Microsoft.DotNet.Build.Tasks.Installers";
+
///
/// Replacement token for license URLs in the generated EULA.
///
@@ -37,6 +59,11 @@ internal abstract class MsiBase
///
internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C");
+ public abstract string ProductTemplate
+ {
+ get;
+ }
+
///
/// Metadata for the MSI such as package ID, version, author information, etc.
///
@@ -59,19 +86,22 @@ protected IBuildEngine BuildEngine
}
///
- /// The directory where the compiler output (.wixobj files) will be generated.
+ /// The root intermediate output directory.
///
- protected string CompilerOutputPath
+ protected string BaseIntermediateOutputPath
{
get;
}
///
- /// The root intermediate output directory.
+ /// When , package references in the generated .wixproj do not include
+ /// version information. This is for repos that rely on CPM and building other installers using
+ /// SDK style projects.
///
- protected string BaseIntermediateOutputPath
+ protected internal bool ManagePackageVersionsCentrally
{
get;
+ set;
}
///
@@ -83,7 +113,7 @@ protected string BaseIntermediateOutputPath
DefaultValues.Manufacturer;
///
- /// The platform of the MSI.
+ /// The platform (bitness) of the MSI.
///
protected string Platform
{
@@ -104,40 +134,117 @@ protected string WixSourceDirectory
}
///
- /// The directory containing the WiX toolset binaries.
+ /// Generate VersionOverride attributes for package references. This avoids conflicts when
+ /// using CPM and a different version of WiX for non-workload related projects in the same repository.
+ ///
+ protected bool OverridePackageVersions
+ {
+ get;
+ }
+
+ ///
+ /// The WiX toolset version. This version applies to both the WiX SDK and any additional toolset
+ /// package references for extensions.
+ ///
+ protected string WixToolsetVersion
+ {
+ get;
+ }
+
+ ///
+ /// When set to , a wixpack archive will be generated when the MSI is compiled.
+ /// The wixpack is used to sign an MSI and its contents when using Arcade.
///
- protected string WixToolsetPath
+ protected bool GenerateWixpack
{
get;
+ set;
}
///
- /// Set of files to include in the NuGet package that will wrap the MSI. Keys represent the source files and the
- /// value contains the relative path inside the generated NuGet package.
+ /// The package version to use when adding package references to the generated .wixproj. Returns
+ /// if is .
+ ///
+ protected string? WixToolsetPackageVersion =>
+ ManagePackageVersionsCentrally ? null : WixToolsetVersion;
+
+ ///
+ /// Set of files to include in the NuGet package that will wrap the MSI. Keys represent source files and
+ /// values contain relative paths inside the generated NuGet package.
///
public Dictionary NuGetPackageFiles { get; set; } = new();
- public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath,
- string platform, string baseIntermediateOutputPath)
+ ///
+ /// The output directory to use when generating a wixpack for signing.
+ ///
+ public string? WixpackOutputDirectory
{
- BuildEngine = buildEngine;
- WixToolsetPath = wixToolsetPath;
- Platform = platform;
- BaseIntermediateOutputPath = baseIntermediateOutputPath;
+ get;
+ init;
+ }
- // 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;
+ ///
+ /// The MSI UpgradeCode.
+ ///
+ protected abstract Guid UpgradeCode
+ {
+ get;
+ }
+
+ ///
+ /// The provider key name used to manage MSI dependents.
+ ///
+ protected abstract string ProviderKeyName
+ {
+ get;
+ }
+
+ ///
+ /// The name of the registry key for tracking installation records used by the CLI and
+ /// and finalizer. May be if the MSI does not support installation
+ /// records.
+ ///
+ protected abstract string? InstallationRecordKey
+ {
+ get;
}
///
- /// Produces an MSI and returns a task item with metadata about the MSI.
+ /// The package type represented by 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);
+ protected abstract string? MsiPackageType
+ {
+ get;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Metadata passed to the task that are used to build the MSI.
+ ///
+ /// The target platform of the MSI.
+ /// The base directory to use when generating the wix project source files.
+ /// The version of the WiX toolset to use for building the installer.
+ /// Determines whether PackageOverride attributes should be generated
+ /// when adding package references to avoid CPM conflicts.
+ /// When set to , package references won't include
+ /// package version information, unless version overrides are enabled.
+ public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine,
+ string platform, string baseIntermediateOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion,
+ bool overridePackageVersions = false, bool generateWixpack = false,
+ string? wixpackOutputDirectory = null, bool managePackageVersionsCentrally = false)
+ {
+ BuildEngine = buildEngine;
+ Platform = platform;
+ BaseIntermediateOutputPath = baseIntermediateOutputPath;
+ WixToolsetVersion = wixToolsetVersion;
+ WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", Path.GetRandomFileName());
+ Metadata = metadata;
+ OverridePackageVersions = overridePackageVersions;
+ GenerateWixpack = generateWixpack;
+ WixpackOutputDirectory = wixpackOutputDirectory;
+ ManagePackageVersionsCentrally = managePackageVersionsCentrally;
+ }
///
/// Gets the platform specific ProductName MSI property.
@@ -150,6 +257,7 @@ protected string GetProductName(string platform) =>
///
/// Generates a EULA (RTF file) that contains the license URL of the underlying NuGet package.
///
+ /// The full path the generated EULA.
protected string GenerateEula()
{
string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf");
@@ -159,81 +267,210 @@ protected string GenerateEula()
}
///
- /// Creates a new compiler tool task and configures some common extensions and preprocessor
- /// variables.
+ /// Creates a basic WiX project using the specific toolset version and sets common properties and
+ /// package references.
+ ///
+ /// An empty project.
+ ///
+ ///
+ /// The following properties are set: InstallerPlatform, SuppressValidation, OutputType, TargetName,
+ /// DebugType
+ ///
+ ///
+ /// The following preprocessor variables are included: InstallerVersion
+ ///
+ ///
+ protected virtual WixProject CreateProject()
+ {
+ if (Directory.Exists(WixSourceDirectory))
+ {
+ Directory.Delete(WixSourceDirectory, true);
+ }
+
+ Directory.CreateDirectory(WixSourceDirectory);
+
+ WixProject wixproj = new(WixToolsetVersion) { OverridePackageVersions = this.OverridePackageVersions };
+
+ // ***********************************************************
+ // Initialize common properties and preprocessor definitions.
+ // ***********************************************************
+ wixproj.AddProperty(WixProperties.InstallerPlatform, Platform);
+ // Pacakge is the default in v5, but defaults can change.
+ wixproj.AddProperty(WixProperties.OutputType, "Package");
+ // Turn off ICE validation. CodeIntegrity and AppLocker block ICE checks that require elevation, even
+ // when running as administator.
+ wixproj.AddProperty(WixProperties.SuppressValidation, "true");
+ // The WiX SDK will determine the extension based on the output type, e.g. Package -> .msi, Patch -> .msp, etc.
+ wixproj.AddProperty(WixProperties.TargetName, Path.GetFileNameWithoutExtension(OutputName));
+ // WiX only supports "full". If the property is overridden (Directory.build.props),
+ // the compiler will report a warning, e.g. "warning WIX1098: The value 'embedded' is not a valid value for command line argument '-pdbType'. Using the value 'full' instead."
+ wixproj.AddProperty(WixProperties.DebugType, "full");
+ wixproj.AddProperty("IntermediateOutputPath", @"obj\$(Configuration)");
+
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Bitness, Platform == "x86" ? "always32" : "always64");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula());
+ // Windows Install 5.0 was released with W2K8 R2 and Windows 7. It's also required to support
+ // arm64. See https://learn.microsoft.com/en-us/windows/win32/msi/released-versions-of-windows-installer
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallerVersion, "500");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer);
+ // The package ID and version used to generate the MSI is stored as properties, but
+ // has no effect on the MSI. It's only purpose is to capture some information about
+ // the source package.
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Metadata.Id);
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductLanguage, "1033");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform));
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{UpgradeCode:B}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, ProviderKeyName);
+
+ if (!string.IsNullOrWhiteSpace(InstallationRecordKey))
+ {
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, InstallationRecordKey);
+ }
+
+ // All workload MSIs must support reference counting since they are shared between multiple
+ // SDKs and Visual Studio.
+ wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetDependencyExtension, WixToolsetPackageVersion);
+ // Util extension is required to access the QueryNativeMachine custom action.
+ wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUtilExtension, WixToolsetPackageVersion);
+ // All workload MSIs (manifests or packs) need to override the default dialog set and select a minimal UI.
+ wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUIExtension, WixToolsetPackageVersion);
+
+ // Extract common template source files used for all workload MSIs.
+ //EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory);
+ //EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract(ProductTemplate, WixSourceDirectory, "Product.wxs");
+
+ return wixproj;
+ }
+
+ ///
+ /// Adds a Files element to harvest files and place them in the specified component group and feature.
///
- ///
- protected CompilerToolTask CreateDefaultCompiler()
+ /// The directory ID containing the directory path for the root of the harvested files.
+ /// The directory to include for harvesting.
+ /// Globbing pattern to use for harvesting. The default is "**", indicating that directories should be recursed.
+ /// The ID of the feature to add the generated component group holding the harvested files.
+ protected void AddFiles(string dirId, string include, string wildcard = "**", string featureId = DefaultValues.PackageContentsFeatureId)
{
- CompilerToolTask candle = new(BuildEngine, WixToolsetPath, CompilerOutputPath, Platform);
+ // Generate sequential templates, e.g., Files00.wxs, Files01.wxs, etc.
+ string idSuffix = $"{_filesCount:D2}";
+ string componentGroupId = $"CG_{idSuffix}";
+ string filesWxs = EmbeddedTemplates.Extract("Files.wxs", WixSourceDirectory, $"Files{idSuffix}.wxs");
- // Required extension to parse the dependency provider authoring.
- candle.AddExtension(WixExtensions.WixDependencyExtension);
+ Utils.StringReplace(filesWxs, Encoding.UTF8,
+ (MsiTokens.__COMPONENT_GROUP_ID__, componentGroupId),
+ (MsiTokens.__DIR_ID__, dirId),
+ (MsiTokens.__INCLUDE__, include + Path.DirectorySeparatorChar + wildcard));
- 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}");
+ AddComponentGroupReferenceToFeature(featureId, componentGroupId);
- return candle;
+ _filesCount++;
}
///
- /// Links the MSI using the output from the WiX compiler using a default set of WiX extensions.
+ /// Creates a new Directory element using the specified ID and name under the specified parent directory.
///
- /// 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)
+ /// The identifier of the directory.
+ /// The name of the directory.
+ /// The identifier of the parent directory.
+ /// The source file used for adding the directory and searching for parent references.
+ protected void AddDirectory(string id, string name, string parentId = "DOTNETHOME", string sourceFile = "Directories.wxs")
{
- return Link(compilerOutputPath, outputFile, iceSuppressions, WixExtensions.WixDependencyExtension,
- WixExtensions.WixUIExtension, WixExtensions.WixUtilExtension);
+ var srcPath = Path.Combine(WixSourceDirectory, sourceFile);
+ var productDoc = XDocument.Load(srcPath);
+ var ns = productDoc.Root!.Name.Namespace;
+
+ var parentDirectoryElement = productDoc.Root.Descendants(ns + "Directory")
+ .FirstOrDefault(f => f.Attribute("Id")?.Value == parentId);
+
+ if (parentDirectoryElement != null)
+ {
+ parentDirectoryElement.Add(new XElement(ns + "Directory", new XAttribute("Id", id), new XAttribute("Name", name)));
+ productDoc.Save(srcPath);
+ }
}
///
- /// Links the MSI using the output from the WiX compiler and a set of WiX extensions.
+ /// Builds the MSI and returns a task item with metadata about the MSI.
///
- /// 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)
+ /// The path containing the directory where the MSI will be generated.
+ /// A task item containing metadata related to the MSI.
+ public virtual ITaskItem Build(string outputPath)
{
- // 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)
+ string wixProjectPath = Path.Combine(WixSourceDirectory, "msi.wixproj");
+ WixProject wixproj = CreateProject();
+ wixproj.AddProperty("OutputPath", outputPath);
+ string directoryBuildTargets = EmbeddedTemplates.Extract("Directory.Build.targets", WixSourceDirectory);
+
+ if (GenerateWixpack)
{
- OutputFile = outputFile,
- SourceFiles = Directory.EnumerateFiles(compilerOutputPath, "*.wixobj"),
- SuppressIces = iceSuppressions == null ? null : string.Join(";", iceSuppressions.Select(i => i.ItemSpec))
- };
+ // Wixpacks need to capture compile time information from the WiX SDK to rebuild the MSI
+ // after replacing any unsigned content when using Arcade to sign.
+ Utils.StringReplace(directoryBuildTargets,
+ Encoding.UTF8, ("__WIXPACK_OUTPUT_DIR__", WixpackOutputDirectory));
+
+ // Add a package reference to pull in the CreateWixBuildWixpack task. The version
+ // should automatically default to the "major.minor.patch-*", e.g. 10.0.0-*
+ wixproj.AddPackageReference(_MicrosoftDotNetBuildTaskInstallers, ToolsetInfo.ArcadeVersion);
+
+ wixproj.AddProperty(WixProperties.GenerateWixpack, "true");
+ }
- foreach (string wixExtension in wixExtensions)
+ if (File.Exists(wixProjectPath))
{
- light.AddExtension(wixExtension);
+ File.Delete(wixProjectPath);
}
- if (!light.Execute())
+ wixproj.Save(wixProjectPath);
+
+ // Use DOTNET_HOST_PATH if set, otherwise, fall back to resolivng the host relative to
+ // the runtime being used.
+ string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
+ if (string.IsNullOrWhiteSpace(dotnetHostPath))
+ {
+ dotnetHostPath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), @"..\..\..\dotnet.exe");
+ }
+
+ if (!File.Exists(dotnetHostPath))
{
- throw new Exception(Strings.FailedToLinkMsi);
+ throw new InvalidOperationException("Unable to find a suitable host.");
}
- TaskItem msiItem = new TaskItem(light.OutputFile);
+ ProcessStartInfo startInfo = new()
+ {
+ FileName = dotnetHostPath,
+ Arguments = $"build {wixProjectPath}",
+ };
- // Return a task item that contains all the information about the generated MSI.
+ var buildProcess = Process.Start(startInfo);
+ buildProcess?.WaitForExit();
+
+ // Return a task item that contains information about the generated MSI.
+ TaskItem msiItem = new TaskItem(Path.Combine(outputPath, OutputName));
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);
+ msiItem.SetMetadata(Workloads.Metadata.PackageType, MsiPackageType);
+
+ var fi = new FileInfo(msiItem.ItemSpec);
+ if (fi.Length > DefaultValues.MaxMsiSize)
+ {
+ throw new IOException($"The generated MSI, {msiItem.ItemSpec}, exceeded the maximum size ({DefaultValues.MaxMsiSize} bytes allowed for workloads.)");
+ }
+
+ if (GenerateWixpack && !string.IsNullOrEmpty(WixpackOutputDirectory))
+ {
+ msiItem.SetMetadata(Workloads.Metadata.Wixpack, Path.Combine(
+ WixpackOutputDirectory,
+ Path.GetFileNameWithoutExtension(OutputName)) + ".msi.wixpack.zip");
+ }
+
+ AddDefaultPackageFiles(msiItem);
return msiItem;
}
@@ -248,6 +485,65 @@ protected void AddDefaultPackageFiles(ITaskItem msi)
NuGetPackageFiles["LICENSE.TXT"] = @"\";
}
+
+ ///
+ /// Creates a source file containing a directory fragment.
+ ///
+ /// The name of the directory.
+ /// The ID of the directory.
+ /// The ID of the directory reference (parent directory).
+
+ protected void AddDirectory2(string name, string id, string reference)
+ {
+ try
+ {
+ AddDirectory(name, id, reference, WixSourceDirectory, $"dir{_dirCount}.wxs");
+ }
+ finally
+ {
+ _dirCount++;
+ }
+ }
+
+ ///
+ /// Adds a reference to a component group within a specified feature in the Product.wxs file.
+ ///
+ /// If the specified feature is not found in the Product.wxs file, no changes are made.
+ /// This method updates the Product.wxs file in the directory specified by WixSourceDirectory.
+ /// The identifier of the feature to which the component group reference will be added. Must match the value of
+ /// the 'Id' attribute of an existing element.
+ /// The identifier of the component group to reference. This value is set as the 'Id' attribute of the new
+ /// element.
+ protected void AddComponentGroupReferenceToFeature(string featureId, string componentGroupId)
+ {
+ var productDoc = XDocument.Load(Path.Combine(WixSourceDirectory, "Product.wxs"));
+ var ns = productDoc.Root!.Name.Namespace;
+
+ var featureElement = productDoc.Root.Descendants(ns + "Feature")
+ .FirstOrDefault(f => f.Attribute("Id")?.Value == featureId);
+
+ if (featureElement != null)
+ {
+ featureElement.Add(new XElement(ns + "ComponentGroupRef", new XAttribute("Id", componentGroupId)));
+ productDoc.Save(Path.Combine(WixSourceDirectory, "Product.wxs"));
+ }
+ }
+
+ ///
+ /// Creates a source file containing a directory fragment.
+ ///
+ /// The name of the directory.
+ /// The ID of the directory.
+ /// The ID of the directory reference (parent directory).
+ /// The source directory to use for the generated fragment.
+ /// The file name of the generated fragment.
+ internal static void AddDirectory(string name, string id, string reference, string sourceDirectory, string fragmentName)
+ {
+ string dirWxs = EmbeddedTemplates.Extract("DirectoryReference.wxs", sourceDirectory, fragmentName);
+
+ Utils.StringReplace(dirWxs, Encoding.UTF8,
+ (MsiTokens.__DIR_REF_ID__, reference), (MsiTokens.__DIR_ID__, id), (MsiTokens.__DIR_NAME__, name));
+ }
}
}
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..d69df353466
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs
@@ -0,0 +1,30 @@
+// 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
+{
+ ///
+ /// Defines token names used to create MSIs.
+ ///
+ internal class MsiTokens
+ {
+ public static readonly string __DIR_REF_ID__ = nameof(__DIR_REF_ID__);
+ public static readonly string __DIR_ID__ = nameof(__DIR_ID__);
+ public static readonly string __DIR_NAME__ = nameof(__DIR_NAME__);
+
+ ///
+ /// Replacement token for Files@Include.
+ ///
+ public static readonly string __INCLUDE__ = nameof(__INCLUDE__);
+
+ ///
+ /// Replacement token for ComponentGroup@Id.
+ ///
+ public static readonly string __COMPONENT_GROUP_ID__ = nameof(__COMPONENT_GROUP_ID__);
+
+ ///
+ /// Replacement token for FeatureRef@Id.
+ ///
+ public static readonly string __FEATURE_REF_ID__ = nameof(__FEATURE_REF_ID__);
+ }
+}
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..f5a8e498c29 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,11 @@ 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 File table.
///
@@ -27,7 +32,7 @@ public class MsiUtils
///
/// 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 +44,26 @@ 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.
///
@@ -139,7 +164,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();
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs
index b8948f2aff2..34f7d9cab03 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs
@@ -1,10 +1,6 @@
// 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.Text;
-
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
///
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..17df42b566c 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
{
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..311f744a379 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
@@ -7,8 +7,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
using System.Text.Json;
+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
@@ -18,6 +21,8 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
///
internal class WorkloadManifestMsi : MsiBase
{
+ public override string ProductTemplate => "ManifestProduct.wxs";
+
public WorkloadManifestPackage Package { get; }
public List WorkloadPackGroups { get; } = new();
@@ -25,57 +30,83 @@ internal class WorkloadManifestMsi : MsiBase
///
protected override string BaseOutputName => Path.GetFileNameWithoutExtension(Package.PackagePath);
+ protected override string? MsiPackageType => DefaultValues.ManifestMsi;
+
///
- /// True if the manifest installer supports side-by-side installs, otherwise the installer
- /// supports major upgrades.
+ /// True if the manifest installer supports side-by-side installs, otherwise it's
+ /// assumed the installer supports major upgrades.
///
- protected bool IsSxS
+ ///
+ /// Major upgrades require both the ProductVersion and ProductCode to change. Refer to the
+ /// Windows Installer for
+ /// more details
+ ///
+ protected bool AllowSideBySideInstalls
{
get;
}
- public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
- string baseIntermediateOutputPath, bool isSxS = false) :
- base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath)
+ // 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 must ensure that the ProductVersion is higher than previously shipped versions.
+ // For SxS installs the UpgradeCode can be a random GUID.
+ protected override Guid UpgradeCode =>
+ AllowSideBySideInstalls ? Guid.NewGuid() :
+ Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}");
+
+ protected override string ProviderKeyName =>
+ AllowSideBySideInstalls ? $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" :
+ $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}";
+
+ protected override string? InstallationRecordKey => "InstalledManifests";
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// The NuGet package containing the workload manifest.
+ /// The target platform of the installer.
+ ///
+ ///
+ /// Determines whether manifest installers are side-by-side for an SDK feature band or support major upgrades.
+ /// The version of the WiX toolset to use for building the installer.
+ /// Determines if VersionOverride attributes are generated for package references.
+ /// Determines if a wixpack archive should be generated. The wixpack is required to sign MSIs using Arcade.
+ /// The directory to use for generating a wixpack for signing.
+ public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine,
+ string baseIntermediateOutputPath, bool allowSideBySideInstalls = false, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion,
+ bool overridePackageVersions = false, bool generateWixpack = false, string? wixpackOutputDirectory = null) :
+ base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediateOutputPath, wixToolsetVersion,
+ overridePackageVersions, generateWixpack, wixpackOutputDirectory)
{
Package = package;
- IsSxS = isSxS;
+ AllowSideBySideInstalls = allowSideBySideInstalls;
}
- ///
- ///
- public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null)
+ ///
+ /// Creates a new WiX project for building a workload manifest installer (MSI).
+ ///
+ protected override WixProject CreateProject()
{
- // 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 = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = packageDataDirectory
- };
+ WixProject wixproj = base.CreateProject();
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
+ // Configure file harvesting.
+ string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data");
+ AddFiles(AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory,
+ packageDataDirectory);
foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f)))
{
NuGetPackageFiles[file] = @"\data\extractedManifest\" + Path.GetFileName(file);
}
- // Add WorkloadPackGroups.json to add to workload manifest MSI
- string? jsonContentWxs = null;
+ // Add WorkloadPackGroups.json to add to workload manifest MSI
string? jsonDirectory = null;
+ // Default the variable to false. If we harvested workload pack group data, we'll override it
+ wixproj.AddPreprocessorDefinition("IncludePackGroupJson", "false");
+
if (WorkloadPackGroups.Any())
{
- jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs");
-
string jsonAsString = JsonSerializer.Serialize(WorkloadPackGroups, typeof(IList), new JsonSerializerOptions() { WriteIndented = true });
jsonDirectory = Path.Combine(WixSourceDirectory, "json");
Directory.CreateDirectory(jsonDirectory);
@@ -83,82 +114,29 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions
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);
- }
-
- 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));
+ AddFiles(AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory,
+ jsonDirectory);
- if (IsSxS)
- {
- candle.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version);
- }
+ wixproj.AddPreprocessorDefinition("IncludePackGroupJson", "true");
+ wixproj.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory);
- if (jsonContentWxs != null)
- {
- candle.AddSourceFiles(jsonContentWxs);
- candle.AddPreprocessorDefinition("IncludePackGroupJson", "true");
- candle.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory);
- }
- else
- {
- candle.AddPreprocessorDefinition("IncludePackGroupJson", "false");
+ NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath);
}
- // 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");
+ // Add manifest directories.
+ AddDirectory("SdkManifestDir", "sdk-manifests");
+ AddDirectory("SdkFeatureBandVersionDir", $"{Package.SdkFeatureBand}", "SdkManifestDir");
// 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()}");
+ AddDirectory("ManifestIdDir", $"{Package.ManifestId.ToLowerInvariant()}", "SdkFeatureBandVersionDir");
- if (!candle.Execute())
+ if (AllowSideBySideInstalls)
{
- throw new Exception(Strings.FailedToCompileMsi);
+ AddDirectory("ManifestVersionDir", Package.GetManifest().Version, "ManifestIdDir");
}
- ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions);
-
- AddDefaultPackageFiles(msi);
-
- return msi;
+ return wixproj;
}
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..f3c563ac3b8 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,6 +1,8 @@
// 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.Collections.Generic;
using System.IO;
@@ -8,6 +10,7 @@
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
+
using Microsoft.NET.Sdk.WorkloadManifestReader;
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
@@ -16,182 +19,106 @@ internal class WorkloadPackGroupMsi : MsiBase
{
WorkloadPackGroupPackage _package;
+ int _dirIdCount = 1;
+
///
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)
- {
- _package = package;
- }
-
- public override ITaskItem Build(string outputPath, ITaskItem[] iceSuppressions)
- {
- List packageContentWxsFiles = new List();
+ public override string ProductTemplate => "Product.wxs";
- int packNumber = 1;
-
- MsiDirectory dotnetHomeDirectory = new MsiDirectory("dotnet", "DOTNETHOME");
- Dictionary sourceDirectoryNamesAndValues = new();
-
- foreach (var pack in _package.Packs)
- {
- string packageContentWxs = Path.Combine(WixSourceDirectory, $"PackageContent.{pack.Id}.wxs");
-
- 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);
+ protected override string ProviderKeyName =>
+ $"{_package.Id},{Metadata.PackageVersion},{Platform}";
- directoryReference = versionDir.Id;
- }
+ protected override string? InstallationRecordKey => "InstalledPackGroups";
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = directoryReference,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = pack.DestinationDirectory,
- SourceVariableName = "SourceDir" + packNumber,
- ComponentGroupName = "CG_PackageContents" + packNumber
- };
+ protected override Guid UpgradeCode =>
+ Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}");
- sourceDirectoryNamesAndValues[heat.SourceVariableName] = heat.SourceDirectory;
+ protected override string? MsiPackageType => DefaultValues.WorkloadPackGroupMsi;
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
+ ///
+ /// Gets a new directory ID.
+ ///
+ protected string DirectoryId => $"dir_{_dirIdCount++:0000}";
- packageContentWxsFiles.Add(packageContentWxs);
+ public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine,
+ string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion,
+ bool overridePackageVersions = false, bool generateWixPack = false)
+ : base(package.GetMsiMetadata(), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion,
+ overridePackageVersions, generateWixPack)
+ {
+ _package = package;
+ }
- packNumber++;
- }
+ protected override WixProject CreateProject()
+ {
+ WixProject wixproj = base.CreateProject();
- // 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);
+ string wixProjectPath = Path.Combine(WixSourceDirectory, "packgroup.wixproj");
- // 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);
+ EmbeddedTemplates.Extract("PackDirectories.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory);
+ EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory);
- // Add registry keys for packs in the pack group.
+ // Extract and modify the installation record. For pack groups, we need to add an entry for each
+ // workload pack installed by the 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}";
-
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}");
- candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPackGroups");
- foreach (var kvp in sourceDirectoryNamesAndValues)
+#nullable disable
+ if (registryDoc != null)
{
- candle.AddPreprocessorDefinition(kvp.Key, kvp.Value);
- }
-
- if (!candle.Execute())
- {
- throw new Exception(Strings.FailedToCompileMsi);
+ var 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}\{pack.PackageVersion}"),
+ new XElement(ns + "RegistryValue", new XAttribute("Value", ""), new XAttribute("Type", "string"))));
+ }
+ registryDoc.Save(registryWxsPath);
}
+#nullable enable
- 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; }
+ int packNumber = 1;
- public Dictionary Subdirectories { get; } = new();
+ HashSet directoryReferences = new();
- public MsiDirectory(string name, string id)
+ foreach (var pack in _package.Packs)
{
- Name = name;
- Id = id;
- }
+ // Calculate the installation directory for the pack and generate a unique reference
+ string packInstallDir = WorkloadPackMsi.GetInstallDir(pack.Kind);
+ string packInstallDirReference = WorkloadPackMsi.GetDirectoryReference(pack.Kind);
- public MsiDirectory GetSubdirectory(string name, string id)
- {
- if (Subdirectories.TryGetValue(name, out var subdir))
+ if (pack.Kind != WorkloadPackKind.Library && pack.Kind != WorkloadPackKind.Template)
{
- if (!subdir.Id.Equals(id, StringComparison.Ordinal))
- {
- throw new ArgumentException($"ID {id} didn't match existing ID {subdir.Id} for directory {name}.");
- }
- return subdir;
+ // Add directories for the package ID and version under the installation folder.
+ string dirId = DirectoryId;
+ AddDirectory(pack.Id, dirId, packInstallDirReference);
+ packInstallDirReference = DirectoryId;
+ AddDirectory(pack.PackageVersion.ToString(), packInstallDirReference, dirId);
}
- subdir = new MsiDirectory(name, id);
- Subdirectories.Add(name, subdir);
- return subdir;
+ string sourceDir = $"SourceDir_{packNumber}";
+ wixproj.AddHarvestDirectory(pack.DestinationDirectory, packInstallDirReference,
+ sourceDir, $"CG_PackageContents_{packNumber}");
+ wixproj.AddPreprocessorDefinition(sourceDir, pack.DestinationDirectory);
+ packNumber++;
}
+#nullable disable
+ // 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 ns2 = productDoc.Root.Name.Namespace;
+ var componentGroupRefElement = productDoc.Root.Descendants(ns2 + "ComponentGroupRef").Single();
+ componentGroupRefElement.ReplaceWith(Enumerable.Range(1, _package.Packs.Count).Select(n => new XElement(ns2 + "ComponentGroupRef", new XAttribute("Id", "CG_PackageContents_" + n))));
+ productDoc.Save(productWxsPath);
+#nullable enable
- 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 wixproj;
}
}
}
+
+#nullable disable
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..2c8eb9bb412 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
@@ -18,71 +18,51 @@ internal class WorkloadPackMsi : MsiBase
///
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;
- }
-
- 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 directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ?
- "InstallDir" : "VersionDir";
+ public override string ProductTemplate => "Product.wxs";
- HarvesterToolTask heat = new(BuildEngine, WixToolsetPath)
- {
- DirectoryReference = directoryReference,
- OutputFile = packageContentWxs,
- Platform = this.Platform,
- SourceDirectory = _package.DestinationDirectory
- };
+ protected override string ProviderKeyName =>
+ $"{_package.Id},{_package.PackageVersion},{Platform}";
- if (!heat.Execute())
- {
- throw new Exception(Strings.HeatFailedToHarvest);
- }
+ protected override Guid UpgradeCode =>
+ Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
- CompilerToolTask candle = CreateDefaultCompiler();
+ protected override string? InstallationRecordKey => "InstalledPacks";
- 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));
+ protected override string? MsiPackageType => DefaultValues.WorkloadPackMsi;
- // Only extract the include file as it's not compilable, but imported by various source files.
- EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
+ public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath,
+ string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion,
+ bool overridePackageVersions = false, bool generateWixPack = false,
+ string? wixpackOutputDirectory = null) :
+ base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion,
+ overridePackageVersions, generateWixPack, wixpackOutputDirectory)
+ {
+ _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}";
+ protected override WixProject CreateProject()
+ {
+ WixProject wixproj = base.CreateProject();
- 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");
+ // Add the default installation directory based on the workload pack kind.
+ AddDirectory("InstallDir", GetInstallDir(_package.Kind));
+ string directoryReference = "InstallDir";
- if (!candle.Execute())
+ if (_package.Kind != WorkloadPackKind.Library && _package.Kind != WorkloadPackKind.Template)
{
- throw new Exception(Strings.FailedToCompileMsi);
+ AddDirectory("PackageDir", Metadata.Id, "InstallDir");
+ AddDirectory("VersionDir", Metadata.PackageVersion.ToString(), "PackageDir");
+ // Override the directory refernece for harvesting.
+ directoryReference = "VersionDir";
}
- ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions);
+ AddFiles(directoryReference, _package.DestinationDirectory);
- AddDefaultPackageFiles(msi);
-
- return msi;
+ return wixproj;
}
///
- /// Get the installation directory based on the kind of workload pack.
+ /// Gets the name of the installation directory based on the kind of workload pack.
///
/// The workload pack kind.
/// The name of the root installation directory.
@@ -95,6 +75,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/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs
index 28a461c2e26..08c27b981ad 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
@@ -6,8 +6,10 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Packaging;
using System.Linq;
using System.Text.Json;
+using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
@@ -17,65 +19,48 @@ internal class WorkloadSetMsi : MsiBase
{
private WorkloadSetPackage _package;
+ public override string ProductTemplate => "WorkloadSetProduct.wxs";
+
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 ProviderKeyName =>
+ $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}";
+
+ protected override string? InstallationRecordKey => "InstalledWorkloadSets";
+
+ protected override Guid UpgradeCode =>
+ Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}");
+
+ protected override string? MsiPackageType => DefaultValues.WorkloadSetMsi;
+
+ public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine,
+ string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion,
+ bool overridePackageVersions = false, bool generateWixPack = false) :
+ base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediatOutputPath,
+ wixToolsetVersion, overridePackageVersions, generateWixPack)
{
_package = package;
}
- public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions)
+ protected override WixProject CreateProject()
{
- // Harvest the package contents before adding it to the source files we need to compile.
- string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs");
+ WixProject wixproj = base.CreateProject();
+
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);
- }
-
- 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));
-
- // 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}";
-
- // 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);
-
- return msi;
+ AddFiles(MsiDirectories.WorkloadSetVersionDirectory, packageDataDirectory);
+
+ AddDirectory("SdkManifestDir", "sdk-manifests");
+ AddDirectory("SdkFeatureBandVersionDir", $"{_package.SdkFeatureBand}", "SdkManifestDir");
+ AddDirectory("WorkloadSetsDir", $"workloadsets", "SdkFeatureBandVersionDir");
+ AddDirectory("WorkloadSetVersionDir", $"{_package.WorkloadSetVersion}", "WorkloadSetsDir");
+
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeStrategy, "none");
+ wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.WorkloadSetVersion, $"{_package.WorkloadSetVersion}");
+
+ return wixproj;
}
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs
index 82010d2552b..516ad70f933 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs
@@ -1,12 +1,10 @@
-
-
-
+
-
-
-
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs
index 09ddad7ddf6..69a1d18bfc9 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs
@@ -1,62 +1,16 @@
-
-
-
+
+
+
-
-
-
-
-
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp
new file mode 100644
index 00000000000..a03187e1fe4
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp
@@ -0,0 +1,33 @@
+
+
+
+
+ $(IntermediateOutputPath)wixpack
+ __WIXPACK_OUTPUT_DIR__
+
+
+
+
+
+
+
+
+
+
+ $(CompilerAdditionalOptions) -bcgg
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs
new file mode 100644
index 00000000000..1e4b8a4c5fa
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs
new file mode 100644
index 00000000000..e9a28b53842
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs
index c20410c0c6f..1519c1c473c 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs
@@ -1,13 +1,13 @@
-
-
-
-
+
+
-
+
-
+
@@ -19,47 +19,37 @@
-
- NOT WIX_DOWNGRADE_DETECTED
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs
new file mode 100644
index 00000000000..9099f628246
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..05ba52ad7ee 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs
@@ -1,11 +1,9 @@
-
-
-
-
-
-
+
+
@@ -13,14 +11,31 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs
index 30bb5ead5b8..9e7c871d427 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs
@@ -1,17 +1,15 @@
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
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/WorkloadPackDirectories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadPackDirectories.wxs
new file mode 100644
index 00000000000..02b6cd8688b
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadPackDirectories.wxs
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs
index 01429f57ff6..ab98a8360a2 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs
@@ -1,41 +1,41 @@
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
-
+
+
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp
new file mode 100644
index 00000000000..a33a6edd2af
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp
@@ -0,0 +1,8 @@
+namespace Microsoft.DotNet.Build.Tasks.Workloads
+{
+ public class ToolsetInfo
+ {
+ public const string MicrosoftWixToolsetVersion = "{MicrosoftWixToolsetSdkVersion}";
+ public const string ArcadeVersion = "{ArcadeVersion}";
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs
new file mode 100644
index 00000000000..7cb5c661cad
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs
@@ -0,0 +1,31 @@
+// 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
+{
+ ///
+ /// Defines well-known package identifiers for WiX toolset packages
+ ///
+ public class ToolsetPackages
+ {
+ ///
+ /// Provides access to Heat tool for harvesting directories, files, etc.
+ ///
+ public const string MicrosoftWixToolsetHeat = "Microsoft.WixToolset.Heat";
+
+ ///
+ /// Provides access to the Util extension, including built-in custom actions.
+ ///
+ public const string MicrosoftWixToolsetUtilExtension = "Microsoft.WixToolset.Util.wixext";
+
+ ///
+ /// Provides access to UI extensions like custom dialog sets for MSIs.
+ ///
+ public const string MicrosoftWixToolsetUIExtension = "Microsoft.WixToolset.UI.wixext";
+
+ ///
+ /// Provides the dependency provider extension to manage shared installations and MSI reference counting.
+ ///
+ public const string MicrosoftWixToolsetDependencyExtension = "Microsoft.WixToolset.Dependency.wixext";
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs
index b858b4bc4f3..a4d78cc6946 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs
@@ -79,6 +79,29 @@ internal static void StringReplace(string fileName, Dictionary t
File.SetAttributes(fileName, oldAttributes);
}
+ ///
+ /// Replaces all the tokens in a file using the provided replacement tokens. Each key-value-pair define the
+ /// token to replace (key) and its replacement (value).
+ ///
+ /// The file to modify.
+ /// The encoding to use when updating the file.
+ /// An array of replacement tokens.
+ internal static void StringReplace(string fileName, Encoding encoding, params (string Key, string Value)[] tokenReplacements)
+ {
+ FileAttributes oldAttributes = File.GetAttributes(fileName);
+ File.SetAttributes(fileName, oldAttributes & ~FileAttributes.ReadOnly);
+
+ string content = File.ReadAllText(fileName);
+
+ foreach (var token in tokenReplacements)
+ {
+ content = content.Replace(token.Key, token.Value);
+ }
+
+ File.WriteAllText(fileName, content, encoding);
+ File.SetAttributes(fileName, oldAttributes);
+ }
+
///
/// Checks whether a string parameter is neither nor empty.
///
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs
index 4439fc1ed61..4314896a6be 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs
@@ -20,7 +20,7 @@ public abstract class VisualStudioWorkloadTaskBase : Task
public static readonly string[] SupportedPlatforms = { "x86", "x64", "arm64" };
///
- /// The root intermediate output directory. This directory serves as a the base for generating
+ /// The root intermediate output directory. This directory serves as the base for generating
/// installer sources and other projects used to create workload artifacts for Visual Studio.
///
[Required]
@@ -41,13 +41,13 @@ public string BaseOutputPath
}
///
- /// A set of Internal Consistency Evaluators (ICEs) to suppress.
+ /// Determines whether a wix pack archive should be generated to sign the MSI using Arcade.
///
- public ITaskItem[] IceSuppressions
+ public bool GenerateWixPack
{
get;
set;
- }
+ } = false;
///
/// A set of items containing all the MSIs that were generated. Additional metadata
@@ -92,6 +92,25 @@ public string WixToolsetPath
set;
}
+ ///
+ /// The version of the WiX toolset to use.
+ ///
+ public string WixToolsetVersion
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Generate VersionOverride attributes for package references. This avoids conflicts when
+ /// using CPM and a different version of WiX for non-workload related projects in the same repository.
+ ///
+ public bool OverridePackageVersions
+ {
+ get;
+ set;
+ }
+
///
/// Core execution of the build task.
///
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs
index 363dea4c64f..3d08fc743c4 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs
@@ -8,10 +8,17 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
///
public static class PreprocessorDefinitionNames
{
+ public static readonly string Bitness = nameof(Bitness);
public static readonly string DependencyProviderKeyName = nameof(DependencyProviderKeyName);
public static readonly string EulaRtf = nameof(EulaRtf);
public static readonly string InstallDir = nameof(InstallDir);
public static readonly string InstallationRecordKey = nameof(InstallationRecordKey);
+
+ ///
+ /// Specifies the Windows Installer version.
+ ///
+ public static readonly string InstallerVersion = nameof(InstallerVersion);
+
public static readonly string ManifestId = nameof(ManifestId);
public static readonly string Manufacturer = nameof(Manufacturer);
public static readonly string PackKind = nameof(PackKind);
@@ -20,10 +27,12 @@ public static class PreprocessorDefinitionNames
public static readonly string Platform = nameof(Platform);
public static readonly string ProductCode = nameof(ProductCode);
public static readonly string ProductName = nameof(ProductName);
+ public static readonly string ProductLanguage = nameof(ProductLanguage);
public static readonly string ProductVersion = nameof(ProductVersion);
public static readonly string SdkFeatureBandVersion = nameof(SdkFeatureBandVersion);
public static readonly string SourceDir = nameof(SourceDir);
public static readonly string UpgradeCode = nameof(UpgradeCode);
+ public static readonly string UpgradeStrategy = nameof(UpgradeStrategy);
public static readonly string WorkloadSetVersion = nameof(WorkloadSetVersion);
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs
new file mode 100644
index 00000000000..fc8690671ea
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs
@@ -0,0 +1,233 @@
+// 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.Xml;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
+{
+ ///
+ /// Record to track HarvestDirectory item metadata consumed by Heat when generating setup authoring.
+ ///
+ /// The directory to harvest.
+ /// The name of the component group to create for generated authoring.
+ /// The ID of the directory reference to use instead of TARGETDIR.
+ /// The preprocessor variable to use instead of SourceDir.
+ /// Suppress generation of registry elements.
+ /// Suppress generation of a Directory element for the parent directory of the file.
+ public record HarvestDirectoryInfo(string Path, string ComponentGroupName, string DirectoryRefId, string PreprocessorVariable,
+ bool SuppressRegistry, bool SuppressRootDirectory);
+
+ ///
+ /// Represents an SDK style WiX project.
+ ///
+ public class WixProject
+ {
+ private const string _attributeComponentGroupName = "ComponentGroupName";
+ private const string _attributeDirectoryRefId = "DirectoryRefId";
+ private const string _attributeInclude = "Include";
+ private const string _attributePreprocessorVariable = "PreprocessorVariable";
+ private const string _attributeSdk = "Sdk";
+ private const string _attributeSuppressRegistry = "SuppressRegistry";
+ private const string _attributeSuppressRootDirectory = "SuppressRootDirectory";
+ private const string _attributeVersion = "Version";
+ private const string _attributeVersionOverride = "VersionOverride";
+ private const string _elementPropertyGroup = "PropertyGroup";
+ private const string _elementProject = "Project";
+ private const string _elementItemGroup = "ItemGroup";
+ private const string _itemHarvestDirectory = "HarvestDirectory";
+ private const string _itemPackageReference = "PackageReference";
+ private const string _propertyDefineConstants = "DefineConstants";
+
+ private const string _defaultSdk = "Microsoft.WixToolset.Sdk";
+
+ private Dictionary _packageReferences = new(StringComparer.OrdinalIgnoreCase);
+
+ // Preprocessor variables are case sensitive.
+ private Dictionary _preprocessorDefinitions = new();
+
+ private Dictionary _properties = new(StringComparer.OrdinalIgnoreCase);
+
+ private Dictionary _harvestDirectories = new(StringComparer.OrdinalIgnoreCase);
+
+ private string _wixToolsetSdk;
+
+ private string _toolsetVersion;
+
+ ///
+ /// Generate VersionOverride attributes for package references.
+ ///
+ public bool OverridePackageVersions
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// The version of the WiX toolset the project will reference.
+ /// The version applies to both the toolset SDK and package references.
+ /// The WiX toolset SDK to use.
+ public WixProject(string toolsetVersion, string wixToolsetSdk = _defaultSdk)
+ {
+ _toolsetVersion = toolsetVersion;
+ _wixToolsetSdk = wixToolsetSdk;
+ }
+
+ ///
+ /// Generates a .wixproj (XML) file using the specified path and current configuration.
+ ///
+ /// The path of the .wixproj to generate.
+ public void Save(string path)
+ {
+ XmlDocument doc = new XmlDocument();
+ var project = doc.CreateElement(_elementProject);
+
+ project.SetAttribute(_attributeSdk, $"{_wixToolsetSdk}/{_toolsetVersion}");
+
+ if (_properties.Count > 0)
+ {
+ var propertyGroup = doc.CreateElement(_elementPropertyGroup);
+
+ foreach (var propertyName in _properties.Keys)
+ {
+ var property = doc.CreateElement(propertyName);
+ property.InnerText = _properties[propertyName];
+ propertyGroup.AppendChild(property);
+ }
+
+ project.AppendChild(propertyGroup);
+ }
+
+ if (_packageReferences.Count > 0)
+ {
+ var packageReferencesItemGroup = doc.CreateElement(_elementItemGroup);
+
+ foreach (string packageId in _packageReferences.Keys)
+ {
+ var item = doc.CreateElement(_itemPackageReference);
+ item.SetAttribute(_attributeInclude, packageId);
+
+ // Allow null/empty versions in case CPM already defined the packages.
+ if (!string.IsNullOrEmpty(_packageReferences[packageId]))
+ {
+ item.SetAttribute(OverridePackageVersions ? _attributeVersionOverride : _attributeVersion,
+ _packageReferences[packageId]);
+ }
+
+ packageReferencesItemGroup.AppendChild(item);
+ }
+
+ project.AppendChild(packageReferencesItemGroup);
+ }
+
+ if (_preprocessorDefinitions.Count > 0)
+ {
+ var preprocessorPropertyGroup = doc.CreateElement(_elementPropertyGroup);
+
+ foreach (string key in _preprocessorDefinitions.Keys)
+ {
+ var defineConstantsProperty = doc.CreateElement(_propertyDefineConstants);
+ defineConstantsProperty.InnerText = $"$({_propertyDefineConstants});{key}={_preprocessorDefinitions[key]}";
+ preprocessorPropertyGroup.AppendChild(defineConstantsProperty);
+ }
+
+ project.AppendChild(preprocessorPropertyGroup);
+ }
+
+ if (_harvestDirectories.Count > 0)
+ {
+ var _harvestDirectoryItemGroup = doc.CreateElement(_elementItemGroup);
+
+ foreach (var harvestInfo in _harvestDirectories.Values)
+ {
+ var item = doc.CreateElement(_itemHarvestDirectory);
+ item.SetAttribute(_attributeInclude, harvestInfo.Path);
+ item.SetAttribute(_attributeComponentGroupName, harvestInfo.ComponentGroupName);
+
+ if (!string.IsNullOrWhiteSpace(harvestInfo.DirectoryRefId))
+ {
+ item.SetAttribute(_attributeDirectoryRefId, harvestInfo.DirectoryRefId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(harvestInfo.PreprocessorVariable))
+ {
+ item.SetAttribute(_attributePreprocessorVariable, harvestInfo.PreprocessorVariable);
+ }
+
+ item.SetAttribute(_attributeSuppressRegistry, harvestInfo.SuppressRegistry.ToString().ToLowerInvariant());
+ item.SetAttribute(_attributeSuppressRootDirectory, harvestInfo.SuppressRootDirectory.ToString().ToLowerInvariant());
+
+ _harvestDirectoryItemGroup.AppendChild(item);
+ }
+
+ project.AppendChild(_harvestDirectoryItemGroup);
+ }
+
+ // Add the root Project node.
+ doc.AppendChild(project);
+
+ XmlWriterSettings settings = new XmlWriterSettings
+ {
+ Indent = true,
+ OmitXmlDeclaration = true
+ };
+
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
+
+ using StreamWriter streamWriter = new(path);
+ using XmlWriter writer = XmlWriter.Create(streamWriter, settings);
+ doc.Save(writer);
+ }
+
+ ///
+ /// Adds a package reference using the specified package identifier and version.
+ ///
+ /// The package identifier to add.
+ /// The version of the package.
+ public void AddPackageReference(string id, string version) =>
+ _packageReferences[id] = version;
+
+ ///
+ /// Adds a package reference using the specified package identifier and implicit toolset version.
+ ///
+ /// The package identifier to add.
+ public void AddPackageReference(string id) =>
+ AddPackageReference(id, _toolsetVersion);
+
+ ///
+ /// Adds a preprocessor definition using the DefineConstants property.
+ ///
+ public void AddPreprocessorDefinition(string name, string value) =>
+ _preprocessorDefinitions[name] = value;
+
+ ///
+ /// Adds an msbuild property.
+ ///
+ /// The name of the property to set.
+ /// The value of the property to set.
+ public void AddProperty(string name, string value) =>
+ _properties[name] = value;
+
+ ///
+ /// Adds a directory for harvesting. A package reference for Heat (the harvesting tool) will automatically
+ /// be added to the project.
+ ///
+ /// The local path of the directory to harvest.
+ /// The ID of the directory to reference instead of TARGETDIR.
+ /// The preprocessor variable to use instead of SourceDir.
+ /// The name of the component group to create for generated authoring.
+ /// Suppress generation of registry elements.
+ /// Suppress generation of a Directory element for the parent directory of the file.
+ public void AddHarvestDirectory(string path, string directoryRefId = null, string preprocessorVariable = null,
+ string componentGroupName = DefaultValues.DefaultComponentGroupName, bool suppressRegistry = true, bool suppressRootDirectory = true)
+ {
+ _harvestDirectories[path] = new HarvestDirectoryInfo(path, componentGroupName, directoryRefId,
+ preprocessorVariable, suppressRegistry, suppressRootDirectory);
+ AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, _toolsetVersion);
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs
new file mode 100644
index 00000000000..9a98ed90409
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs
@@ -0,0 +1,42 @@
+// 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.Wix
+{
+ ///
+ /// Property names used inside a WiX project (.wixproj).
+ ///
+ internal class WixProperties
+ {
+ ///
+ /// The platform of the installer being built.
+ ///
+ public static readonly string InstallerPlatform = nameof(InstallerPlatform);
+
+ ///
+ /// Turns off validation (ICE) when set to true.
+ ///
+ public static readonly string SuppressValidation = nameof(SuppressValidation);
+
+ ///
+ /// The name of the output produced by the .wixproj. The extension is determined by the WiX SDK
+ /// based on the output type.
+ ///
+ public static readonly string TargetName = nameof(TargetName);
+
+ ///
+ /// The type of output produced by the project, for example, Package produces an MSI, Patch produces an MSP, etc.
+ ///
+ public static readonly string OutputType = nameof(OutputType);
+
+ ///
+ /// The debug information to emit.
+ ///
+ public static readonly string DebugType = nameof(DebugType);
+
+ ///
+ /// Boolean property indicating whether to generate WiX pack used for signing.
+ ///
+ public static readonly string GenerateWixpack = nameof(GenerateWixpack);
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs
index dde510726d3..a07f2f5f436 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs
@@ -35,7 +35,7 @@ public string[] Platforms
get;
}
- public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platforms, string destinationBaseDirectory,
+ public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platforms, string destinationBaseDirectory,
ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : base(packagePath, destinationBaseDirectory, shortNames, log)
{
_pack = pack;
@@ -100,6 +100,46 @@ public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platf
}
}
+ ///
+ /// Gets the package associated with a specific workload pack for the specified platform.
+ ///
+ /// The path where workload packages reside.
+ ///
+ ///
+ internal static string? GetSourcePackage(string packageSource, WorkloadPack pack, string platform)
+ {
+ if (pack.IsAlias && pack.AliasTo != null)
+ {
+ foreach (string rid in pack.AliasTo.Keys)
+ {
+ string sourcePackage = Path.Combine(packageSource, $"{pack.AliasTo[rid]}.{pack.Version}.nupkg");
+
+ switch (rid)
+ {
+ case "win7":
+ case "win10":
+ case "win":
+ case "any":
+ return sourcePackage;
+ default:
+ if (rid == $"win-{platform}")
+ {
+ return sourcePackage;
+ }
+ // Unsupported RID.
+ continue;
+ }
+ }
+ }
+ else
+ {
+ // For non-RID specific packs we'll produce MSIs for each supported platform.
+ return Path.Combine(packageSource, $"{pack.Id}.{pack.Version}.nupkg");
+ }
+
+ return null;
+ }
+
///
/// Creates a workload pack package from the provided NuGet package and workload pack.
///
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs
index 08213015640..01c05683907 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs
@@ -200,7 +200,13 @@ public WorkloadPackageBase(string packagePath, string destinationBaseDirectory,
Title = nuspec.GetTitle();
PackagePath = packagePath;
- DestinationDirectory = Path.Combine(destinationBaseDirectory, $"{Identity}");
+
+ // Previously this extracted to a directory containing the package identity, but for testing
+ // inside Arcade under v5, Heat reports errors for workload packs like Emscripten that
+ // have deep structure.
+ // heat.exe : error HEAT0001: System.Runtime.InteropServices.COMException (0x00000003): Failed to get short
+ // path buffer size for file: D:\git\forks\arcade\artifacts\bin\Microsoft.DotNet.Build.Tasks.Workloads.Tests\Debug\net10.0\TEST_OUTPUT\qbdhgr2p.dpg\pkg\Microsoft.NET.Runtime.Emscripten.2.0.23.Sdk.win-x64.6.0.4\tools\emscripten\cache\sysroot\include\c++\v1\__support\win32\limits_msvc_win32.h
+ DestinationDirectory = Path.Combine(destinationBaseDirectory, Path.GetRandomFileName());
ShortNames = shortNames;
PackageFileName = Path.GetFileNameWithoutExtension(packagePath);