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