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