diff --git a/eng/Versions.props b/eng/Versions.props index c43d67ed817..da4fe8a88af 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -51,11 +51,15 @@ 6.0.4 6.0.4 6.0.4 + 9.0.100-baseline.1.23464.1 6.0.4 8.0.0-preview.6.23326.2 6.0.3 6.0.4 + 6.0.21 + 6.0.22 15.2.302-preview.14.122 + 16.0.527 7.0.0 8.0.0-preview.4.23259.5 diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs new file mode 100644 index 00000000000..4815c41fe2c --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using FluentAssertions; +using Microsoft.Arcade.Test.Common; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + public class CreateVisualStudioWorkloadSetTests : TestBase + { + [WindowsOnlyFact] + public static 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"); + + if (Directory.Exists(baseIntermediateOutputPath)) + { + Directory.Delete(baseIntermediateOutputPath, recursive: true); + } + + ITaskItem[] workloadSetPackages = new[] + { + 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(); + + CreateVisualStudioWorkloadSet createWorkloadSetTask = new CreateVisualStudioWorkloadSet() + { + BaseOutputPath = BaseOutputPath, + BaseIntermediateOutputPath = baseIntermediateOutputPath, + BuildEngine = buildEngine, + PackageSource = TestAssetsPath, + WixToolsetPath = WixToolsetPath, + WorkloadSetPackageFiles = workloadSetPackages + }; + + Assert.True(createWorkloadSetTask.Execute()); + + // Spot check the x64 generated MSI. + ITaskItem msi = createWorkloadSetTask.Msis.Where(i => i.GetMetadata(Metadata.Platform) == "x64").FirstOrDefault(); + Assert.NotNull(msi); + + // 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"); + + // Workload sets are SxS. Verify that we don't have an Upgrade table. + Assert.False(MsiUtils.HasTable(msi.ItemSpec, "Upgrade")); + + // Verify the SWIX authoring for one of the workload set MSIs. + ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\x64")).FirstOrDefault(); + Assert.Equal(DefaultValues.PackageTypeMsiWorkloadSet, workloadSetSwixItem.GetMetadata(Metadata.PackageType)); + + string msiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(workloadSetSwixItem.ItemSpec), "msi.swr")); + Assert.Contains("package name=Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1", msiSwr); + Assert.Contains("version=12.8.45", msiSwr); + Assert.DoesNotContain("vs.package.chip=x64", msiSwr); + Assert.Contains("vs.package.machineArch=x64", msiSwr); + Assert.Contains("vs.package.type=msi", msiSwr); + + // Verify package group SWIX project + ITaskItem workloadSetPackageGroupSwixItem = createWorkloadSetTask.SwixProjects.Where( + s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeWorkloadSetPackageGroup)). + FirstOrDefault(); + 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 cbdbe0e290a..e95fd0db6b2 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { + [Collection("Workload Creation")] public class CreateVisualStudioWorkloadTests : TestBase { [WindowsOnlyFact] 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 db42e1a9fd7..72719b864a0 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 @@ -25,13 +25,15 @@ - + + + @@ -43,12 +45,16 @@ + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index a510b1cd94b..f17c0319469 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -104,8 +104,8 @@ public void ItCanBuildATemplatePackMsi() 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); pkg.Extract(); - WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); - + var buildEngine = new MockBuildEngine(); + WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetPath, BaseIntermediateOutputPath); ITaskItem item = msi.Build(MsiOutputPath); string msiPath = item.GetMetadata(Metadata.FullPath); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs index 579a0e66a28..1d626d57d35 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs @@ -2,15 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; -using Microsoft.DotNet.Build.Tasks.Workloads; +using System.IO; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Deployment.DotNet.Releases; -using System.IO; +using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -22,13 +18,35 @@ public void ItCanReadAManifestPackage() { string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); - TaskItem manifestPackageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + TaskItem manifestPackageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.300.6.0.22.nupkg")); WorkloadManifestPackage p = new(manifestPackageItem, PackageRootDirectory, new Version("1.2.3")); - ReleaseVersion expectedFeatureBand = new("6.0.200"); + ReleaseVersion expectedFeatureBand = new("6.0.300"); Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain", p.ManifestId); Assert.Equal(expectedFeatureBand, p.SdkFeatureBand); } + + [WindowsOnlyTheory] + [InlineData("Microsoft.NET.Workload.Emscripten.net6.Manifest-8.0.100-alpha.1", WorkloadManifestPackage.ManifestSeparator, "8.0.100-alpha.1")] + [InlineData("Microsoft.NET.Workload.Emscripten.Manifest-8.0.100-alpha.1.23062.6", WorkloadManifestPackage.ManifestSeparator, "8.0.100-alpha.1.23062.6")] + [InlineData("Microsoft.NET.Workloads.8.0.100-preview.7.23376.3", WorkloadSetPackage.SdkFeatureBandSeparator, "8.0.100-preview.7.23376.3")] + [InlineData("Microsoft.NET.Workloads.8.0.100", WorkloadSetPackage.SdkFeatureBandSeparator, "8.0.100")] + public static void ItExtractsTheSdkVersionFromThePackageId(string packageId, string separator, string expectedVersion) + { + string actualSdkVersion = WorkloadPackageBase.GetSdkVersion(packageId, separator); + + Assert.Equal(expectedVersion, actualSdkVersion); + } + + [WindowsOnlyFact] + public void ItThrowsIfTheMsiVersionIsInvalid() + { + string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "wls-pkg"); + + ITaskItem workloadSetPackageItem = new TaskItem(Path.Combine(TestAssetsPath, "microsoft.net.workloads.9.0.100.9.0.100-baseline.1.23464.1.nupkg")); + + Assert.Throws(() => { WorkloadSetPackage p = new(workloadSetPackageItem, PackageRootDirectory, new Version("256.12.3")); }); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageGroupTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageGroupTests.cs index 06aef31e000..e0a47e07da7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageGroupTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageGroupTests.cs @@ -2,17 +2,11 @@ // 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 Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.Deployment.DotNet.Releases; -using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.DotNet.Build.Tasks.Workloads.Swix; -using Microsoft.NET.Sdk.WorkloadManifestReader; -using NuGet.Packaging.Core; using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests @@ -31,8 +25,9 @@ public void ItGeneratesPackageGroupsForManifestPackages(string manifestPackageFi string destinationBaseDirectory = Path.Combine(BaseIntermediateOutputPath, destinationDirectory); TaskItem manifestPackageItem = new(Path.Combine(TestAssetsPath, manifestPackageFilename)); WorkloadManifestPackage manifestPackage = new(manifestPackageItem, destinationBaseDirectory, msiVersion, shortNames, null, isSxS: true); - var packageGroup = SwixPackageGroup.Create(manifestPackage); - var packageGroupItem = PackageGroupSwixProject.CreateProjectItem(packageGroup, BaseIntermediateOutputPath, BaseOutputPath); + var packageGroup = new SwixPackageGroup(manifestPackage); + var packageGroupItem = PackageGroupSwixProject.CreateProjectItem(packageGroup, BaseIntermediateOutputPath, BaseOutputPath, + DefaultValues.PackageTypeManifestPackageGroup); // Verify package group expectations Assert.Equal(expectedPackageId, packageGroup.Name); @@ -45,14 +40,14 @@ public void ItGeneratesPackageGroupsForManifestPackages(string manifestPackageFi // Verify the task item metadata Assert.Equal(expectedFeatureBand, packageGroupItem.GetMetadata(Metadata.SdkFeatureBand)); - Assert.Equal(DefaultValues.PackageTypePackageGroup, packageGroupItem.GetMetadata(Metadata.PackageType)); + Assert.Equal(DefaultValues.PackageTypeManifestPackageGroup, packageGroupItem.GetMetadata(Metadata.PackageType)); } public static readonly IEnumerable PackageGroupData = new List { - new object[] { "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg", "grp1", - new Version("1.2.3"), s_shortNames, "PackageGroup.Mono.ToolChain.Manifest-6.0.200", new Version("1.2.3"), - " vs.dependency id=Mono.ToolChain.Manifest-6.0.200.6.0.3", "6.0.200" }, + new object[] { "microsoft.net.workload.mono.toolchain.manifest-6.0.300.6.0.21.nupkg", "grp1", + new Version("1.2.3"), s_shortNames, "PackageGroup.Mono.ToolChain.Manifest-6.0.300", new Version("1.2.3"), + " vs.dependency id=Mono.ToolChain.Manifest-6.0.300.6.0.21", "6.0.300" }, new object[] { "microsoft.net.workload.emscripten.net6.manifest-8.0.100-preview.6.8.0.0-preview.6.23326.2.nupkg", "grp2", new Version("1.2.3"), s_shortNames, "PackageGroup.Emscripten.net6.Manifest-8.0.100", new Version("1.2.3"), " vs.dependency id=Emscripten.net6.Manifest-8.0.100-preview.6.8.0.0-preview.6.23326.2", "8.0.100-preview.6" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs index 69a724318a2..99b98e1b112 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { + [Collection("SWIX Package")] public class SwixPackageTests : TestBase { [WindowsOnlyFact] @@ -34,33 +35,16 @@ public void ItThrowsIfPackageRelativePathExceedsLimit() Assert.Equal(@"Relative package path exceeds the maximum length (182): Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100,version=6.0.0.0,chip=x64,productarch=neutral,machinearch=x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi.", e.Message); } - [WindowsOnlyFact] - public void SwixPackageIdsIncludeThePackageVersion() - { - // Build to a different path to avoid any file read locks on the MSI from other tests - // that can open it. - string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, Path.GetRandomFileName()); - 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); - pkg.Extract(); - WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); - - ITaskItem item = msi.Build(MsiOutputPath); - - Assert.Equal("Microsoft.iOS.Templates.15.2.302-preview.14.122", item.GetMetadata(Metadata.SwixPackageId)); - } - [WindowsOnlyFact] public void ItOnlyIncludesDefinedPropertiesForMsiPackages() { // Build to a different path to avoid any file read locks on the MSI from other tests // that can open it. + string packageVersion = "16.0.527"; string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, Path.GetRandomFileName()); - string packagePath = Path.Combine(TestAssetsPath, "microsoft.ios.templates.15.2.302-preview.14.122.nupkg"); + string packagePath = Path.Combine(TestAssetsPath, $"microsoft.ios.templates.{packageVersion}.nupkg"); - WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null); + WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), packageVersion, WorkloadPackKind.Template, null); TemplatePackPackage pkg = new(p, packagePath, new[] { "x64" }, PackageRootDirectory); pkg.Extract(); WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); @@ -68,7 +52,7 @@ public void ItOnlyIncludesDefinedPropertiesForMsiPackages() ITaskItem msiItem = msi.Build(MsiOutputPath); msiItem.SetMetadata(Metadata.Platform, "x64"); - Assert.Equal("Microsoft.iOS.Templates.15.2.302-preview.14.122", msiItem.GetMetadata(Metadata.SwixPackageId)); + Assert.Equal($"Microsoft.iOS.Templates.{packageVersion}", msiItem.GetMetadata(Metadata.SwixPackageId)); MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath, new ReleaseVersion("6.0.100"), 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 1ed556310b9..c94be8ce7b3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -19,39 +19,13 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads /// /// An MSBuild task used to create workload artifacts including MSIs and SWIX projects for Visual Studio Installer. /// - public class CreateVisualStudioWorkload : Task + public class CreateVisualStudioWorkload : VisualStudioWorkloadTaskBase { /// /// Used to track which feature bands support the machineArch property. /// private Dictionary _supportsMachineArch = new(); - /// - /// A set of all supported MSI platforms. - /// - public static readonly string[] SupportedPlatforms = { "x86", "x64", "arm64" }; - - /// - /// The root intermediate output directory. This directory serves as a the base for generating - /// installer sources and other projects used to create workload artifacts for Visual Studio. - /// - [Required] - public string BaseIntermediateOutputPath - { - get; - set; - } - - /// - /// The root output directory to use for compiled artifacts such as MSIs. - /// - [Required] - public string BaseOutputPath - { - get; - set; - } - /// /// A set of items that provide metadata associated with the Visual Studio components derived from /// workload manifests. @@ -72,15 +46,6 @@ public bool EnableSideBySideManifests set; } = false; - /// - /// A set of Internal Consistency Evaluators (ICEs) to suppress. - /// - public ITaskItem[] IceSuppressions - { - get; - set; - } - /// /// Determines whether the component (and related packs) should be flagged as /// out-of-support in Visual Studio. @@ -100,38 +65,6 @@ public string ManifestMsiVersion set; } - /// - /// A set of items containing all the MSIs that were generated. Additional metadata - /// is provided for the projects that need to be built to produce NuGet packages for - /// the MSI. - /// - [Output] - public ITaskItem[] Msis - { - get; - protected set; - } - - /// - /// The output path where MSIs will be placed. - /// - private string MsiOutputPath => Path.Combine(BaseOutputPath, "msi"); - - /// - /// The directory to use for locating workload pack packages. - /// - [Required] - public string PackageSource - { - get; - set; - } - - /// - /// Root directory where packages are extracted. - /// - private string PackageRootDirectory => Path.Combine(BaseIntermediateOutputPath, "pkg"); - /// /// A set of items used to shorten the names and identifiers of setup packages. /// @@ -141,27 +74,6 @@ public ITaskItem[] ShortNames set; } - /// - /// A set of items containing .swixproj files that can be build to generate - /// Visual Studio Installer components for workloads. - /// - [Output] - public ITaskItem[] SwixProjects - { - get; - protected set; - } - - /// - /// The directory containing the WiX toolset binaries. - /// - [Required] - public string WixToolsetPath - { - get; - set; - } - /// /// A set of packages containing workload manifests. /// @@ -208,329 +120,266 @@ public bool AllowMissingPacks set; } = false; - public override bool Execute() + protected override bool ExecuteCore() { - try + // TODO: trim out duplicate manifests. + List manifestPackages = new(); + List manifestMsisToBuild = new(); + HashSet swixComponents = new(); + HashSet swixPackageGroups = new(); + Dictionary buildData = new(); + Dictionary packGroupPackages = new(); + + // First construct sets of everything that needs to be built. This includes + // all the packages (manifests and workload packs) that need to be extracted along + // with the different installer types. + foreach (ITaskItem workloadManifestPackageFile in WorkloadManifestPackageFiles) { - // TODO: trim out duplicate manifests. - List manifestPackages = new(); - List manifestMsisToBuild = new(); - HashSet swixComponents = new(); - HashSet swixPackageGroups = new(); - Dictionary buildData = new(); - Dictionary packGroupPackages = new(); - - // First construct sets of everything that needs to be built. This includes - // all the packages (manifests and workload packs) that need to be extracted along - // with the different installer types. - foreach (ITaskItem workloadManifestPackageFile in WorkloadManifestPackageFiles) + // 1. Process the manifest package and create a set of installers. + WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, + string.IsNullOrWhiteSpace(ManifestMsiVersion) ? null : new Version(ManifestMsiVersion), ShortNames, Log, EnableSideBySideManifests); + manifestPackages.Add(manifestPackage); + + if (!_supportsMachineArch.ContainsKey(manifestPackage.SdkFeatureBand)) + { + // Log the original setting and manifest that created the machineArch setting for the featureband. + Log.LogMessage(MessageImportance.Low, $"Setting {nameof(_supportsMachineArch)} to {manifestPackage.SupportsMachineArch} for {Path.GetFileName(manifestPackage.PackageFileName)}"); + _supportsMachineArch[manifestPackage.SdkFeatureBand] = manifestPackage.SupportsMachineArch; + } + else if (_supportsMachineArch[manifestPackage.SdkFeatureBand] != manifestPackage.SupportsMachineArch) { - // 1. Process the manifest package and create a set of installers. - WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, - string.IsNullOrWhiteSpace(ManifestMsiVersion) ? null : new Version(ManifestMsiVersion), ShortNames, Log, EnableSideBySideManifests); - manifestPackages.Add(manifestPackage); + // If multiple manifest packages for the same feature band have conflicting machineArch values + // then we'll treat it as an warning. It will likely fail the build. + Log.LogWarning($"{_supportsMachineArch} was previously set to {_supportsMachineArch[manifestPackage.SdkFeatureBand]}"); + } - if (!_supportsMachineArch.ContainsKey(manifestPackage.SdkFeatureBand)) - { - // Log the original setting and manifest that created the machineArch setting for the featureband. - Log.LogMessage(MessageImportance.Low, $"Setting {nameof(_supportsMachineArch)} to {manifestPackage.SupportsMachineArch} for {Path.GetFileName(manifestPackage.PackageFileName)}"); - _supportsMachineArch[manifestPackage.SdkFeatureBand] = manifestPackage.SupportsMachineArch; - } - else if (_supportsMachineArch[manifestPackage.SdkFeatureBand] != manifestPackage.SupportsMachineArch) - { - // If multiple manifest packages for the same feature band have conflicting machineArch values - // then we'll treat it as an warning. It will likely fail the build. - Log.LogWarning($"{_supportsMachineArch} was previously set to {_supportsMachineArch[manifestPackage.SdkFeatureBand]}"); - } + Dictionary manifestMsisByPlatform = new(); + foreach (string platform in SupportedPlatforms) + { + var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, EnableSideBySideManifests); + manifestMsisToBuild.Add(manifestMsi); + manifestMsisByPlatform[platform] = manifestMsi; + } + + // If we're supporting SxS manifests, generate a package group to wrap the manifest VS packages + // so we don't deal with unstable package IDs during VS insertions. + if (EnableSideBySideManifests) + { + SwixPackageGroup packageGroup = new(manifestPackage); - Dictionary manifestMsisByPlatform = new(); - foreach (string platform in SupportedPlatforms) + if (!swixPackageGroups.Add(packageGroup)) { - var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, EnableSideBySideManifests); - manifestMsisToBuild.Add(manifestMsi); - manifestMsisByPlatform[platform] = manifestMsi; + Log.LogError(Strings.ManifestPackageGroupExists, manifestPackage.Id, packageGroup.Name); } + } - // If we're supporting SxS manifests, generate a package group to wrap the manifest VS packages - // so we don't deal with unstable package IDs during VS insertions. - if (EnableSideBySideManifests) + // 2. Process the manifest itself to determine the set of packs involved and create + // installers for all the packs. Duplicate packs will be ignored, example, when + // workloads in two manifests targeting different feature bands contain the + // same pack dependencies. Building multiple copies of MSIs will cause + // problems (ref counting, repair operations, etc.) and also increases the build time. + // + // When building multiple manifests, it's possible for feature bands to have + // different sets of packs. For example, the same manifest for different feature bands + // can add additional platforms that requires generating additional SWIX projects, while + // ensuring that the pack and MSI is only generated once. + WorkloadManifest manifest = manifestPackage.GetManifest(); + + List packGroupJsonList = new(); + + foreach (WorkloadDefinition workload in manifest.Workloads.Values) + { + if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null)) { - SwixPackageGroup packageGroup = SwixPackageGroup.Create(manifestPackage); + Dictionary> packsInWorkloadByPlatform = new(); - if (!swixPackageGroups.Add(packageGroup)) + string packGroupId = null; + WorkloadPackGroupJson packGroupJson = null; + if (CreateWorkloadPackGroups) { - Log.LogError(Strings.ManifestPackageGroupExists, manifestPackage.Id, packageGroup.Name); + packGroupId = WorkloadPackGroupPackage.GetPackGroupID(workload.Id); + packGroupJson = new WorkloadPackGroupJson() + { + GroupPackageId = packGroupId, + GroupPackageVersion = manifestPackage.PackageVersion.ToString() + }; + packGroupJsonList.Add(packGroupJson); } - } - // 2. Process the manifest itself to determine the set of packs involved and create - // installers for all the packs. Duplicate packs will be ignored, example, when - // workloads in two manifests targeting different feature bands contain the - // same pack dependencies. Building multiple copies of MSIs will cause - // problems (ref counting, repair operations, etc.) and also increases the build time. - // - // When building multiple manifests, it's possible for feature bands to have - // different sets of packs. For example, the same manifest for different feature bands - // can add additional platforms that requires generating additional SWIX projects, while - // ensuring that the pack and MSI is only generated once. - WorkloadManifest manifest = manifestPackage.GetManifest(); - - List packGroupJsonList = new(); - - foreach (WorkloadDefinition workload in manifest.Workloads.Values) - { - if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null)) + foreach (WorkloadPackId packId in wd.Packs) { - Dictionary> packsInWorkloadByPlatform = new(); + WorkloadPack pack = manifest.Packs[packId]; - string packGroupId = null; - WorkloadPackGroupJson packGroupJson = null; if (CreateWorkloadPackGroups) { - packGroupId = WorkloadPackGroupPackage.GetPackGroupID(workload.Id); - packGroupJson = new WorkloadPackGroupJson() + packGroupJson.Packs.Add(new WorkloadPackJson() { - GroupPackageId = packGroupId, - GroupPackageVersion = manifestPackage.PackageVersion.ToString() - }; - packGroupJsonList.Add(packGroupJson); + PackId = pack.Id, + PackVersion = pack.Version + }); } - foreach (WorkloadPackId packId in wd.Packs) + foreach ((string sourcePackage, string[] platforms) in WorkloadPackPackage.GetSourcePackages(PackageSource, pack)) { - WorkloadPack pack = manifest.Packs[packId]; - - if (CreateWorkloadPackGroups) + if (!File.Exists(sourcePackage)) { - packGroupJson.Packs.Add(new WorkloadPackJson() + if (AllowMissingPacks) { - PackId = pack.Id, - PackVersion = pack.Version - }); - } - - foreach ((string sourcePackage, string[] platforms) in WorkloadPackPackage.GetSourcePackages(PackageSource, pack)) - { - if (!File.Exists(sourcePackage)) - { - if (AllowMissingPacks) - { - Log.LogMessage($"Pack {sourcePackage} - {string.Join(",", platforms)} could not be found, it will be skipped."); - continue; - } - else - { - throw new FileNotFoundException(message: "NuGet package not found", fileName: sourcePackage); - } - } - - // Create new build data and add the pack if we haven't seen it previously. - if (!buildData.ContainsKey(sourcePackage)) - { - buildData[sourcePackage] = new BuildData(WorkloadPackPackage.Create(pack, sourcePackage, platforms, PackageRootDirectory, - ShortNames, Log)); + Log.LogMessage($"Pack {sourcePackage} - {string.Join(",", platforms)} could not be found, it will be skipped."); + continue; } - - foreach (string platform in platforms) + else { - // If we haven't seen the platform, create a new entry, then add - // the current feature band. This allows us to track platform specific packs - // across multiple feature bands and manifests. - if (!buildData[sourcePackage].FeatureBands.ContainsKey(platform)) - { - buildData[sourcePackage].FeatureBands[platform] = new(); - } - - _ = buildData[sourcePackage].FeatureBands[platform].Add(manifestPackage.SdkFeatureBand); - - if (!packsInWorkloadByPlatform.ContainsKey(platform)) - { - packsInWorkloadByPlatform[platform] = new(); - } - packsInWorkloadByPlatform[platform].Add(buildData[sourcePackage].Package); - } - - // TODO: Find a better way to track this - if (SkipRedundantMsiCreation) - { - buildData.Remove(sourcePackage); + throw new FileNotFoundException(message: "NuGet package not found", fileName: sourcePackage); } } - } - if (CreateWorkloadPackGroups) - { - // TODO: Support passing in data to skip creating pack groups for certain packs (possibly EMSDK, because it's large) - foreach (var kvp in packsInWorkloadByPlatform) + // Create new build data and add the pack if we haven't seen it previously. + if (!buildData.ContainsKey(sourcePackage)) { - string platform = kvp.Key; + buildData[sourcePackage] = new BuildData(WorkloadPackPackage.Create(pack, sourcePackage, platforms, PackageRootDirectory, + ShortNames, Log)); + } - // The key is the paths 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)) + foreach (string platform in platforms) + { + // If we haven't seen the platform, create a new entry, then add + // the current feature band. This allows us to track platform specific packs + // across multiple feature bands and manifests. + if (!buildData[sourcePackage].FeatureBands.ContainsKey(platform)) { - groupPackage = new WorkloadPackGroupPackage(workload.Id); - groupPackage.Packs.AddRange(kvp.Value); - packGroupPackages[uniquePackGroupKey] = groupPackage; + buildData[sourcePackage].FeatureBands[platform] = new(); } - if (!groupPackage.ManifestsPerPlatform.ContainsKey(platform)) + _ = buildData[sourcePackage].FeatureBands[platform].Add(manifestPackage.SdkFeatureBand); + + if (!packsInWorkloadByPlatform.ContainsKey(platform)) { - groupPackage.ManifestsPerPlatform[platform] = new(); + packsInWorkloadByPlatform[platform] = new(); } - groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage); + packsInWorkloadByPlatform[platform].Add(buildData[sourcePackage].Package); } - foreach (var manifestMsi in manifestMsisByPlatform.Values) + // TODO: Find a better way to track this + if (SkipRedundantMsiCreation) { - manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList); + buildData.Remove(sourcePackage); } } + } - // Finally, add a component for the workload in Visual Studio. - SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId, - ComponentResources, ShortNames); - // Create an additional component for shipping previews - SwixComponent previewComponent = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId, - ComponentResources, ShortNames, "pre"); - - // Check for duplicates, e.g. manifests that were copied without changing workload definition IDs and - // provide a more usable error message so users can track down the duplication. - if (!swixComponents.Add(component)) + if (CreateWorkloadPackGroups) + { + // TODO: Support passing in data to skip creating pack groups for certain packs (possibly EMSDK, because it's large) + foreach (var kvp in packsInWorkloadByPlatform) { - Log.LogError(Strings.WorkloadComponentExists, workload.Id, component.Name); + string platform = kvp.Key; + + // The key is the paths 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)) + { + groupPackage = new WorkloadPackGroupPackage(workload.Id); + groupPackage.Packs.AddRange(kvp.Value); + packGroupPackages[uniquePackGroupKey] = groupPackage; + } + + if (!groupPackage.ManifestsPerPlatform.ContainsKey(platform)) + { + groupPackage.ManifestsPerPlatform[platform] = new(); + } + groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage); } - if (!swixComponents.Add(previewComponent)) + foreach (var manifestMsi in manifestMsisByPlatform.Values) { - Log.LogError(Strings.WorkloadComponentExists, workload.Id, previewComponent.Name); + manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList); } } - } - } - List msiItems = new(); - List swixProjectItems = new(); + // Finally, add a component for the workload in Visual Studio. + SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId, + ComponentResources, ShortNames); + // Create an additional component for shipping previews + SwixComponent previewComponent = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId, + ComponentResources, ShortNames, "pre"); - _ = Parallel.ForEach(buildData.Values, data => - { - // 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 => - { - 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()); - - lock (msiItems) + // Check for duplicates, e.g. manifests that were copied without changing workload definition IDs and + // provide a more usable error message so users can track down the duplication. + if (!swixComponents.Add(component)) { - msiItems.Add(msiOutputItem); + Log.LogError(Strings.WorkloadComponentExists, workload.Id, component.Name); } - foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) + if (!swixComponents.Add(previewComponent)) { - // 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(); + Log.LogError(Strings.WorkloadComponentExists, workload.Id, previewComponent.Name); + } + } + } + } - ITaskItem swixProjectItem = new TaskItem(swixProj); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); - swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + List msiItems = new(); + List swixProjectItems = new(); - lock (swixProjectItems) - { - swixProjectItems.Add(swixProjectItem); - } - } - } - }); - }); + _ = Parallel.ForEach(buildData.Values, data => + { + // Extract the contents of the workload pack package. + Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath)); + data.Package.Extract(); - // 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 => + // Enumerate over the platforms and build each MSI once. + _ = Parallel.ForEach(data.FeatureBands.Keys, platform => { - foreach (var pack in packGroup.Packs) + 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()); + + lock (msiItems) { - pack.Extract(); + msiItems.Add(msiOutputItem); } - foreach (var platform in packGroup.ManifestsPerPlatform.Keys) + foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) { - WorkloadPackGroupMsi msi = new(packGroup, 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()); - - lock (msiItems) - { - msiItems.Add(msiOutputItem); - } - - if (UseWorkloadPackGroupsForVS) + // 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)) { - PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage => - { - // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch - if (_supportsMachineArch[manifestPackage.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) - { - MsiSwixProject swixProject = _supportsMachineArch[manifestPackage.SdkFeatureBand] ? - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) : - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio); - string swixProj = swixProject.Create(); + 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, $"{manifestPackage.SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); - swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + 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); - } - } - }); + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } } } }); + }); - // 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 => + // 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 => + { + foreach (var pack in packGroup.Packs) { - ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + pack.Extract(); + } - // 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)) - { - // Generate SWIX authoring for the MSI package. Do not flag manifest MSI packages for out-of-support. - // These are typically pulled in through .NET SDK components. - MsiSwixProject swixProject = _supportsMachineArch[msi.Package.SdkFeatureBand] ? - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)) : - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform)); - ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiManifest); - swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); - - lock (swixProjectItems) - { - swixProjectItems.Add(swixProjectItem); - } - } + foreach (var platform in packGroup.ManifestsPerPlatform.Keys) + { + WorkloadPackGroupMsi msi = new(packGroup, 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); @@ -540,46 +389,103 @@ public override bool Execute() { msiItems.Add(msiOutputItem); } - }); - // Generate SWIX projects for the Visual Studio components. These are driven by the manifests, so - // they need to be ordered based on feature bands to avoid pulling in unnecessary packs into the drop - // artifacts. - _ = Parallel.ForEach(swixComponents, swixComponent => + if (UseWorkloadPackGroupsForVS) + { + PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage => + { + // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch + if (_supportsMachineArch[manifestPackage.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) + { + MsiSwixProject swixProject = _supportsMachineArch[manifestPackage.SdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio); + string swixProj = swixProject.Create(); + + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{manifestPackage.SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); + swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } + } + }); + } + } + }); + + // 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); + + // 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)) { - ComponentSwixProject swixComponentProject = new(swixComponent, BaseIntermediateOutputPath, BaseOutputPath, IsOutOfSupportInVisualStudio); - ITaskItem swixProjectItem = new TaskItem(swixComponentProject.Create()); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{swixComponent.SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeComponent); - swixProjectItem.SetMetadata(Metadata.IsPreview, swixComponent.Name.EndsWith(".pre").ToString().ToLowerInvariant()); + // Generate SWIX authoring for the MSI package. Do not flag manifest MSI packages for out-of-support. + // These are typically pulled in through .NET SDK components. + MsiSwixProject swixProject = _supportsMachineArch[msi.Package.SdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform)); + ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiManifest); + swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); lock (swixProjectItems) { swixProjectItems.Add(swixProjectItem); } - }); + } - if (EnableSideBySideManifests) + // 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) { - // Generate SWIX projects for the Visual Studio package groups. - _ = Parallel.ForEach(swixPackageGroups, swixPackageGroup => - { - lock (swixProjectItems) - { - swixProjectItems.Add(PackageGroupSwixProject.CreateProjectItem(swixPackageGroup, BaseIntermediateOutputPath, BaseOutputPath)); - } - }); + msiItems.Add(msiOutputItem); } + }); - Msis = msiItems.ToArray(); - SwixProjects = swixProjectItems.ToArray(); - } - catch (Exception e) + // Generate SWIX projects for the Visual Studio components. These are driven by the manifests, so + // they need to be ordered based on feature bands to avoid pulling in unnecessary packs into the drop + // artifacts. + _ = Parallel.ForEach(swixComponents, swixComponent => + { + ComponentSwixProject swixComponentProject = new(swixComponent, BaseIntermediateOutputPath, BaseOutputPath, IsOutOfSupportInVisualStudio); + ITaskItem swixProjectItem = new TaskItem(swixComponentProject.Create()); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{swixComponent.SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeComponent); + swixProjectItem.SetMetadata(Metadata.IsPreview, swixComponent.Name.EndsWith(".pre").ToString().ToLowerInvariant()); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } + }); + + if (EnableSideBySideManifests) { - Log.LogError(e.ToString()); + // Generate SWIX projects for the Visual Studio package groups. + _ = Parallel.ForEach(swixPackageGroups, swixPackageGroup => + { + lock (swixProjectItems) + { + swixProjectItems.Add(PackageGroupSwixProject.CreateProjectItem(swixPackageGroup, BaseIntermediateOutputPath, BaseOutputPath, + DefaultValues.PackageTypeManifestPackageGroup)); + } + }); } - return !Log.HasLoggedErrors; + Msis = msiItems.ToArray(); + SwixProjects = swixProjectItems.ToArray(); + + return true; } static void PossiblyParallelForEach(bool runInParallel, IEnumerable source, Action body) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs new file mode 100644 index 00000000000..97ea915591e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs @@ -0,0 +1,111 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Microsoft.DotNet.Build.Tasks.Workloads.Swix; +using Parallel = System.Threading.Tasks.Parallel; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Build task for generating workload set MSI installers, including projects for + /// building the NuGet package wrappers and SWIX projects for inserting into Visual Studio. + /// + public class CreateVisualStudioWorkloadSet : VisualStudioWorkloadTaskBase + { + /// + /// The version to assign to workload set installers. + /// + public string WorkloadSetMsiVersion + { + get; + set; + } + + /// + /// Set of NuGet packages containing workload sets. + /// + public ITaskItem[] WorkloadSetPackageFiles + { + get; + set; + } + + protected override bool ExecuteCore() + { + Version msiVersion = string.IsNullOrWhiteSpace(WorkloadSetMsiVersion) ? null : new Version(WorkloadSetMsiVersion); + List workloadSetMsisToBuild = new(); + List msiItems = new(); + List swixProjectItems = new(); + HashSet swixPackageGroups = new(); + + foreach (ITaskItem workloadSetPackageFile in WorkloadSetPackageFiles) + { + WorkloadSetPackage workloadSetPackage = new(workloadSetPackageFile, PackageRootDirectory, + msiVersion, shortNames: null, Log); + + foreach (string platform in SupportedPlatforms) + { + var workloadSetMsi = new WorkloadSetMsi(workloadSetPackage, platform, BuildEngine, + WixToolsetPath, BaseIntermediateOutputPath); + workloadSetMsisToBuild.Add(workloadSetMsi); + } + + SwixPackageGroup packageGroup = new(workloadSetPackage); + + if (!swixPackageGroups.Add(packageGroup)) + { + Log.LogError(Strings.ManifestPackageGroupExists, workloadSetPackage.Id, packageGroup.Name); + } + + Log.LogMessage(MessageImportance.High, "Extracting workload set"); + workloadSetPackage.Extract(); + + _ = Parallel.ForEach(workloadSetMsisToBuild, msi => + { + 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()); + + lock (msiItems) + { + msiItems.Add(msiOutputItem); + } + + // 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)); + + string swixProj = swixProject.Create(); + + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{workloadSetPackage.SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiWorkloadSet); + swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } + }); + + foreach (var swixPackageGroup in swixPackageGroups) + { + swixProjectItems.Add(PackageGroupSwixProject.CreateProjectItem(swixPackageGroup, BaseIntermediateOutputPath, BaseOutputPath, + DefaultValues.PackageTypeWorkloadSetPackageGroup)); + } + } + + Msis = msiItems.ToArray(); + SwixProjects = swixProjectItems.ToArray(); + + return true; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index 3dcb1062033..7fd2709c466 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -8,6 +8,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads /// internal static class DefaultValues { + /// + /// Prefix used in Visual Studio for SWIX based package group. + /// + public const string PackageGroupPrefix = "PackageGroup"; + /// /// The default category to assign to a SWIX component. The value is used /// to group individual components in Visual Studio Installer. @@ -42,6 +47,16 @@ internal static class DefaultValues /// /// A value indicating that the SWIX project creates a package group for a workload manifest. /// - public static readonly string PackageTypePackageGroup = "manifest-package-group"; + public static readonly string PackageTypeManifestPackageGroup = "manifest-package-group"; + + /// + /// A value indicating that the SWIX project creates a package group for a workload manifest. + /// + public static readonly string PackageTypeWorkloadSetPackageGroup = "workloadset-package-group"; + + /// + /// A value indicating that the SWIX project creates an MSI package for a workload set. + /// + public static readonly string PackageTypeMsiWorkloadSet = "msi-workload-set"; } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index 3652e5df683..61c5f9f553c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -71,6 +71,7 @@ static EmbeddedTemplates() { "Directories.wxs", $"{ns}.MsiTemplate.Directories.wxs" }, { "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" }, { "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" }, + { "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" }, { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, { "Variables.wxi", $"{ns}.MsiTemplate.Variables.wxi" }, 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 3de6b586b91..057f660adaa 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 @@ -40,8 +40,8 @@ public string DefaultDir /// /// Creates a new instance from the specified . /// - /// The file record obtained from querying the MSI File table. - /// A single file row. + /// The file record obtained from querying the MSI Directory table. + /// A single directory row. public static DirectoryRow Create(Record directoryRecord) { return new DirectoryRow 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 1bbff93a890..0e5c1df333c 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 @@ -104,13 +104,17 @@ protected string WixSourceDirectory } /// - /// The path w + /// The directory containing the WiX toolset binaries. /// protected string WixToolsetPath { get; } + /// + /// 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. + /// public Dictionary NuGetPackageFiles { get; set; } = new(); public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs new file mode 100644 index 00000000000..1091751ed95 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiDirectories.wix.cs @@ -0,0 +1,26 @@ +// 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 well-known directory identifiers for workload MSIs. + /// + internal class MsiDirectories + { + /// + /// The directory reference to use when harvesting the package contents for upgradable manifest installers + /// + public static readonly string ManifestIdDirectory = "ManifestIdDir"; + + /// + /// Directory reference to use when harvesting the package contents for SxS manifest installers. + /// + public static readonly string ManifestVersionDirectory = "ManifestVersionDir"; + + /// + /// Directory reference to use when harvesting package contents for workload sets. + /// + public static readonly string WorkloadSetVersionDirectory = "WorkloadSetVersionDir"; + } +} 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 a604e2896ca..4e7d29f738b 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 @@ -34,6 +34,11 @@ public class MsiUtils /// private const string _getDirectoriesQuery = "SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"; + /// + /// Query string to retrieve all rows from the MSI Registry table. + /// + private const string _getRegistryQuery = "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`"; + /// /// Gets an enumeration of all the files inside an MSI. /// @@ -76,6 +81,27 @@ public static IEnumerable GetAllDirectories(string packagePath) return directories; } + /// + /// Gets an enumeration of all the registry keys inside an MSI. + /// + /// The path of the MSI package to query. + /// An enumeration of all the registry keys. + public static IEnumerable GetAllRegistryKeys(string packagePath) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + using View view = db.OpenView(_getRegistryQuery); + List registryKeys = new(); + view.Execute(); + + foreach (Record directoryRecord in view) + { + registryKeys.Add(RegistryRow.Create(directoryRecord)); + } + + return registryKeys; + } + /// /// Gets an enumeration describing related products defined in the Upgrade table of an MSI /// @@ -155,5 +181,44 @@ public static Version GetVersion(string packagePath) => /// The number of bytes required to install the MSI. public static long GetInstallSize(string packagePath, double factor = 1.4) => GetAllFiles(packagePath).Sum(f => Convert.ToInt64(f.FileSize * factor)); + + /// + /// Validates that a represents a valid MSI ProductVersion. + /// + /// The version to validate. + /// + public static void ValidateProductVersion(Version version) + { + // See to https://learn.microsoft.com/en-us/windows/win32/msi/productversion for additional information. + + if (version.Major > 255) + { + throw new ArgumentOutOfRangeException(string.Format(Strings.MsiProductVersionOutOfRange, nameof(version.Major), 255)); + } + + if (version.Minor > 255) + { + throw new ArgumentOutOfRangeException(string.Format(Strings.MsiProductVersionOutOfRange, nameof(version.Minor), 255)); + } + + if (version.Build > ushort.MaxValue) + { + throw new ArgumentOutOfRangeException(string.Format(Strings.MsiProductVersionOutOfRange, nameof(version.Build), ushort.MaxValue)); + } + } + + /// + /// Determines if the MSI contains a specific table. + /// + /// The path to the MSI package. + /// The name of the table. + /// if the table exists; otherwise. + public static bool HasTable(string packagePath, string tableName) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + + return db.Tables.Contains(tableName); + } } } 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 new file mode 100644 index 00000000000..e258c2ebabf --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Deployment.WindowsInstaller; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines a single row inside the Registry table. + /// + public class RegistryRow + { + /// + /// The preferred root key for the value. + /// + public int Root + { + get; + set; + } + + /// + /// Localizable key for the registry value. + /// + public string Key + { + get; + set; + } + + /// + /// The registry value name. May be null if data is written to the default key. + /// + public string Name + { + get; + set; + } + + /// + /// Formatted field containing the registry value. + /// + public string Value + { + get; + set; + } + + /// + /// Creates a new instance from the specified . + /// + /// The registry record obtained from querying the MSI Registry table. + /// A single registry row. + public static RegistryRow Create(Record registryRecord) + { + return new RegistryRow + { + Root = (int)registryRecord["Root"], + Key = (string)registryRecord["Key"], + Name = (string)registryRecord["Name"], + Value = (string)registryRecord["Value"] + }; + } + } +} 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 b85dfd655e6..6b4cb55f575 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 @@ -20,16 +20,6 @@ internal class WorkloadManifestMsi : MsiBase { public WorkloadManifestPackage Package { get; } - /// - /// The directory reference to use when harvesting the package contents for upgradable manifest installers - /// - private static readonly string s_ManifestIdDirectory = "ManifestIdDir"; - - /// - /// Directory reference to use when harvesting the package contents for SxS installers. - /// - private static readonly string s_ManifestVersionDirectory = "ManifestVersionDir"; - public List WorkloadPackGroups { get; } = new(); /// @@ -62,7 +52,7 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) { - DirectoryReference = IsSxS ? s_ManifestVersionDirectory : s_ManifestIdDirectory, + DirectoryReference = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, OutputFile = packageContentWxs, Platform = this.Platform, SourceDirectory = packageDataDirectory @@ -95,7 +85,7 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions HarvesterToolTask jsonHeat = new(BuildEngine, WixToolsetPath) { - DirectoryReference = IsSxS ? s_ManifestVersionDirectory : s_ManifestIdDirectory, + DirectoryReference = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, OutputFile = jsonContentWxs, Platform = this.Platform, SourceDirectory = jsonDirectory, 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 new file mode 100644 index 00000000000..36c25eba9ee --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -0,0 +1,83 @@ +// 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; +using System.Linq; +using System.Text.Json; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + internal class WorkloadSetMsi : MsiBase + { + private WorkloadSetPackage _package; + + 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) + { + _package = package; + } + + public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions) + { + // Harvest the package contents before adding it to the source files we need to compile. + string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs"); + string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); + + HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) + { + DirectoryReference = MsiDirectories.WorkloadSetVersionDirectory, + OutputFile = packageContentWxs, + Platform = this.Platform, + SourceDirectory = packageDataDirectory + }; + + if (!heat.Execute()) + { + throw new Exception(Strings.HeatFailedToHarvest); + } + + 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.PackageVersion}"); + 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; + } + } +} + +#nullable disable 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 e40ac5bb203..09ddad7ddf6 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -5,15 +5,7 @@ - - - - - - - - - + @@ -21,4 +13,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 d27f73cf3de..90f20707873 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -26,8 +26,8 @@ - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs new file mode 100644 index 00000000000..01429f57ff6 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs index 4960fcf763a..471219c020c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs @@ -159,6 +159,24 @@ internal static string HeatFailedToHarvest { } } + /// + /// Looks up a localized string similar to Invalid workload set package: {0}. The package does not contain a "data" directory.. + /// + internal static string InvalidWorkloadSetPackageMissingDataDir { + get { + return ResourceManager.GetString("InvalidWorkloadSetPackageMissingDataDir", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid workload set package: {0}. The package does not contain any workload set files.. + /// + internal static string InvalidWorkloadSetPackageNoWorkloadSet { + get { + return ResourceManager.GetString("InvalidWorkloadSetPackageNoWorkloadSet", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duplicate manifest ID: {0}. A SWIX package group, {1}, with the same ID already exists.. /// @@ -169,11 +187,20 @@ internal static string ManifestPackageGroupExists { } /// - /// Looks up a localized string similar to Unable to determine the version of the manifest installer. The {0} task should either provide a value for the {1} parameter, or the {2} items should set the {3} metadata.. + /// Looks up a localized string similar to The {0} field of an MSI ProductVersion must be less than or equal to {1}.. /// - internal static string NoManifestInstallerVersion { + internal static string MsiProductVersionOutOfRange { get { - return ResourceManager.GetString("NoManifestInstallerVersion", resourceCulture); + return ResourceManager.GetString("MsiProductVersionOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to determine the version of the installer (MSI). The {0} task should either provide a value for the {1} parameter, or the {2} items should set the {3} metadata.. + /// + internal static string NoInstallerVersion { + get { + return ResourceManager.GetString("NoInstallerVersion", resourceCulture); } } @@ -240,6 +267,15 @@ internal static string UnknownWorkloadKind { } } + /// + /// Looks up a localized string similar to A non-workload set file was found inside the workload set package: {0}. The file will be removed and excluded from the workload set installer.. + /// + internal static string WarnNonWorkloadSetFileFound { + get { + return ResourceManager.GetString("WarnNonWorkloadSetFileFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duplicate workload ID: {0}. A SWIX component, {1}, with the same workload ID already exists.. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx index 9d64ed82282..9ed0343354d 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx @@ -150,11 +150,20 @@ Failed to harvest package contents. + + Invalid workload set package: {0}. The package does not contain a "data" directory. + + + Invalid workload set package: {0}. The package does not contain any workload set files. + Duplicate manifest ID: {0}. A SWIX package group, {1}, with the same ID already exists. - - Unable to determine the version of the manifest installer. The {0} task should either provide a value for the {1} parameter, or the {2} items should set the {3} metadata. + + The {0} field of an MSI ProductVersion must be less than or equal to {1}. + + + Unable to determine the version of the installer (MSI). The {0} task should either provide a value for the {1} parameter, or the {2} items should set the {3} metadata. A SWIX package group must have at least one dependency, Id: {0}. @@ -177,6 +186,9 @@ Unknown workload kind: {0}. + + A non-workload set file was found inside the workload set package: {0}. The file will be removed and excluded from the workload set installer. + Duplicate workload ID: {0}. A SWIX component, {1}, with the same workload ID already exists. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/PackageGroupSwixProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/PackageGroupSwixProject.wix.cs index 21858925e0c..ebbc3f051a6 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/PackageGroupSwixProject.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/PackageGroupSwixProject.wix.cs @@ -72,14 +72,16 @@ public override string Create() /// The package group to use when generating the task item. /// The root intermediate output directory used for generating files. /// The base output directory for storing the compiled SWIX project's output (JSON manifest). + /// The metadata value for the package group. This is used for batching and selection during builds. /// A task item describing the SWIX project. - public static ITaskItem CreateProjectItem(SwixPackageGroup swixPackageGroup, string baseIntermediateOutputPath, string baseOutputPath) + public static ITaskItem CreateProjectItem(SwixPackageGroup swixPackageGroup, string baseIntermediateOutputPath, string baseOutputPath, + string packageGroupType) { PackageGroupSwixProject swixPackageGroupProject = new(swixPackageGroup, baseIntermediateOutputPath, baseOutputPath); ITaskItem swixProjectItem = new TaskItem(swixPackageGroupProject.Create()); swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{swixPackageGroup.SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypePackageGroup); + swixProjectItem.SetMetadata(Metadata.PackageType, packageGroupType); swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); return swixProjectItem; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageBase.cs index cc0751546bd..1def29da771 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageBase.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix { @@ -21,16 +18,27 @@ public IList Dependencies public bool HasDependencies => Dependencies.Count > 0; + /// + /// The name (ID) of the SWIX package. + /// public string Name { get; } + /// + /// The version of the SWIX package. + /// public Version Version { get; } + /// + /// Creates a new instance. + /// + /// The name (ID) of the package. + /// The version of the package. public SwixPackageBase(string name, Version version) { Name = name; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageGroup.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageGroup.wix.cs index a39f178d782..e7d8c00d856 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageGroup.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixPackageGroup.wix.cs @@ -1,23 +1,21 @@ // 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.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.Deployment.DotNet.Releases; namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix { /// /// Represents a Visual Studio package group. Package groups are non-selectable - /// entities + /// entities that can provide stable identities when inserting non-stable packages into Visual Studio. Other + /// package types like components can then reference the stable package group even when the underlying + /// package identity keeps changing. /// internal class SwixPackageGroup : SwixPackageBase { /// - /// The SDK feature band associated with this component. + /// The SDK feature band associated with the package group. The feature band is used for partition builds + /// and grouping SWIX manifests. /// public ReleaseVersion SdkFeatureBand { @@ -25,22 +23,32 @@ public ReleaseVersion SdkFeatureBand } /// - /// Creates a new instance. + /// Creates a new for a workload manifest package. /// - /// The name of the package group. - /// The version of the package group - public SwixPackageGroup(ReleaseVersion sdkFeatureBand, string name, Version version) : base(name, version) + /// The package to reference from the package group. + public SwixPackageGroup(WorkloadManifestPackage package) : this(package, package.SdkFeatureBand, package.SwixPackageGroupId) { - SdkFeatureBand = sdkFeatureBand; } - public static SwixPackageGroup Create(WorkloadManifestPackage manifestPackage) + /// + /// Creates a new for a workload set package. + /// + /// The package to reference from the package group. + public SwixPackageGroup(WorkloadSetPackage package) : this(package, package.SdkFeatureBand, package.SwixPackageGroupId) { - var packageGroup = new SwixPackageGroup(manifestPackage.SdkFeatureBand, manifestPackage.SwixPackageGroupId, manifestPackage.MsiVersion); - - packageGroup.Dependencies.Add(new SwixDependency(manifestPackage.SwixPackageId, manifestPackage.MsiVersion)); + } - return packageGroup; + /// + /// Creates a new and adds the specified package as a dependency. + /// + /// The package to reference from the package group. + /// The SDK feature band to associate with the group. + /// The name (ID) of the package group. + private SwixPackageGroup(WorkloadPackageBase package, ReleaseVersion sdkFeatureBand, string packageGroupName) : + base(packageGroupName, package.MsiVersion) + { + SdkFeatureBand = sdkFeatureBand; + Dependencies.Add(new SwixDependency(package.SwixPackageId, package.MsiVersion)); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs new file mode 100644 index 00000000000..8cba24f014d --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Microsoft.DotNet.Build.Tasks.Workloads.Swix; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Base class used for build tasks that generate MSI installers that + /// can be shipped with Visual Studio or used by the CLI using NuGet packages. + /// + public abstract class VisualStudioWorkloadTaskBase : Task + { + /// + /// A set of all supported MSI platforms. + /// + public static readonly string[] SupportedPlatforms = { "x86", "x64", "arm64" }; + + /// + /// The root intermediate output directory. This directory serves as a the base for generating + /// installer sources and other projects used to create workload artifacts for Visual Studio. + /// + [Required] + public string BaseIntermediateOutputPath + { + get; + set; + } + + /// + /// The root output directory to use for compiled artifacts such as MSIs. + /// + [Required] + public string BaseOutputPath + { + get; + set; + } + + /// + /// A set of Internal Consistency Evaluators (ICEs) to suppress. + /// + public ITaskItem[] IceSuppressions + { + get; + set; + } + + /// + /// A set of items containing all the MSIs that were generated. Additional metadata + /// is provided for the projects that need to be built to produce NuGet packages for + /// the MSI. + /// + [Output] + public ITaskItem[] Msis + { + get; + protected set; + } + + /// + /// The output path where MSIs will be placed. + /// + protected string MsiOutputPath => Path.Combine(BaseOutputPath, "msi"); + + /// + /// Root directory where packages are extracted. + /// + protected string PackageRootDirectory => Path.Combine(BaseIntermediateOutputPath, "pkg"); + + /// + /// The directory to use for locating workload pack packages. + /// + [Required] + public string PackageSource + { + get; + set; + } + + /// + /// A set of items containing .swixproj files that can be build to generate + /// Visual Studio Installer components for workloads. + /// + [Output] + public ITaskItem[] SwixProjects + { + get; + protected set; + } + + /// + /// The directory containing the WiX toolset binaries. + /// + [Required] + public string WixToolsetPath + { + get; + set; + } + + /// + /// Core execution of the build task. + /// + /// if successful; otherwise . + protected abstract bool ExecuteCore(); + + public sealed override bool Execute() + { + try + { + return ExecuteCore(); + } + catch (Exception e) + { + Log.LogError(e.ToString()); + } + + return !Log.HasLoggedErrors; + } + } +} 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 413c4a0702c..363dea4c64f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs @@ -24,5 +24,6 @@ public static class PreprocessorDefinitionNames 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 WorkloadSetVersion = nameof(WorkloadSetVersion); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs index d9fd01c41b1..13b30b37f74 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.NET.Sdk.WorkloadManifestReader; namespace Microsoft.DotNet.Build.Tasks.Workloads @@ -21,15 +22,10 @@ internal class WorkloadManifestPackage : WorkloadPackageBase /// public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; - /// - /// Prefix used in Visual Studio for SWIX based package group. - /// - private const string PackageGroupPrefix = "PackageGroup"; - /// /// Special separator value used in workload manifest package IDs. /// - private const string ManifestSeparator = ".Manifest-"; + internal const string ManifestSeparator = ".Manifest-"; /// /// The filename and extension of the workload manifest file. @@ -67,14 +63,6 @@ public bool SupportsMachineArch get; } - /// - /// The package ID to use when wrapping the manifest MSI inside a package group. - /// - public string SwixPackageGroupId - { - get; - } - /// /// Creates a new instance of a . /// @@ -89,23 +77,9 @@ public WorkloadManifestPackage(ITaskItem package, string destinationBaseDirector ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null, bool isSxS = false) : base(package.ItemSpec, destinationBaseDirectory, shortNames, log) { - if (!string.IsNullOrWhiteSpace(package.GetMetadata(Metadata.MsiVersion))) - { - // We prefer version information on the manifest package item. - MsiVersion = new(package.GetMetadata(Metadata.MsiVersion)); - } - else if (msiVersion != null) - { - // Fall back to the version provided by the task parameter, e.g. if all manifests follow the same versioning rules. - MsiVersion = msiVersion; - } - else - { - // While we could use the major.minor.patch part of the package, manifests are upgradable, so we want - // the user to be aware of this and explicitly tell us the value. - throw new Exception(string.Format(Strings.NoManifestInstallerVersion, nameof(CreateVisualStudioWorkload), - nameof(CreateVisualStudioWorkload.ManifestMsiVersion), nameof(CreateVisualStudioWorkload.WorkloadManifestPackageFiles), Metadata.MsiVersion)); - } + MsiVersion = GetMsiVersion(package, msiVersion, nameof(CreateVisualStudioWorkload), + nameof(CreateVisualStudioWorkload.ManifestMsiVersion), nameof(CreateVisualStudioWorkload.WorkloadManifestPackageFiles)); + MsiUtils.ValidateProductVersion(MsiVersion); SdkFeatureBand = GetSdkFeatureBandVersion(GetSdkVersion(Id)); ManifestId = GetManifestId(Id); @@ -117,7 +91,7 @@ public WorkloadManifestPackage(ITaskItem package, string destinationBaseDirector // in the package ID. For example, if the package ID is "Microsoft.NET.Workload.Emscripten.net6.Manifest-8.0.100-preview.6" // then we want the package group to be "Microsoft.NET.Workload.Emscripten.net6.Manifest-8.0.100". The group would still point // to the versioned SWIX package wrapping the MSI. - SwixPackageGroupId = $"{PackageGroupPrefix}.{ManifestId.Replace(shortNames)}.Manifest-{SdkFeatureBand.ToString(3)}"; + SwixPackageGroupId = $"{DefaultValues.PackageGroupPrefix}.{ManifestId.Replace(shortNames)}.Manifest-{SdkFeatureBand.ToString(3)}"; SupportsMachineArch = bool.TryParse(package.GetMetadata(Metadata.SupportsMachineArch), out bool supportsMachineArch) ? supportsMachineArch : false; } @@ -154,33 +128,6 @@ public WorkloadManifest GetManifest() return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(workloadManifestFile), File.OpenRead(workloadManifestFile), workloadManifestFile); } - /// - /// Converts a string containing an SDK version to a semantic version that normalizes the patch level and - /// optionally includes the first two prerelease labels. For example, if the specified version is 6.0.105, then - /// 6.0.100 would be returned. If the version is 6.0.301-preview.2.1234, the result would be 6.0.300-preview.1. - /// - /// A string containing an SDK version. - /// An SDK feature band version. - internal static ReleaseVersion GetSdkFeatureBandVersion(string sdkVersion) - { - ReleaseVersion version = new(sdkVersion); - - // Ignore CI and dev builds. - if (string.IsNullOrEmpty(version.Prerelease) || version.Prerelease.Split('.').Any(s => string.Equals("ci", s) || string.Equals("dev", s))) - { - return new ReleaseVersion(version.Major, version.Minor, version.SdkFeatureBand); - } - - string[] preleaseParts = version.Prerelease.Split('.'); - - // Only the first two prerelease identifiers are used to support side-by-side previews. - string prerelease = (preleaseParts.Length > 1) ? - $"{preleaseParts[0]}.{preleaseParts[1]}" : - preleaseParts[0]; - - return new ReleaseVersion(version.Major, version.Minor, version.SdkFeatureBand, prerelease); - } - /// /// Extracts the SDK version from the package ID. /// @@ -188,9 +135,7 @@ internal static ReleaseVersion GetSdkFeatureBandVersion(string sdkVersion) /// SDK version part of the package ID. /// internal static string GetSdkVersion(string packageId) => - !string.IsNullOrWhiteSpace(packageId) && packageId.IndexOf(ManifestSeparator) > -1 ? - packageId.Substring(packageId.IndexOf(ManifestSeparator) + ManifestSeparator.Length) : - throw new FormatException(string.Format(Strings.CannotExtractSdkVersionFromPackageId, packageId)); + GetSdkVersion(packageId, ManifestSeparator); /// /// Extracts the manifest ID from the package ID. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs index df7408f9dd4..08213015640 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs @@ -11,6 +11,7 @@ using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -31,11 +32,17 @@ public string Authors get; } + /// + /// The NuGet package copyright. + /// public string Copyright { get; } + /// + /// The NuGet package description. + /// public string Description { get; @@ -138,6 +145,15 @@ public string SwixPackageId protected set; } + /// + /// The SWIX identifier for a package group that references this SWIX package. + /// + public string SwixPackageGroupId + { + get; + protected set; + } + /// /// Gets an instance of a class containing task logging methods. /// @@ -190,11 +206,12 @@ public WorkloadPackageBase(string packagePath, string destinationBaseDirectory, PackageFileName = Path.GetFileNameWithoutExtension(packagePath); ShortName = PackageFileName.Replace(shortNames); SwixPackageId = $"{Id.Replace(shortNames)}.{Identity.Version}"; + SwixPackageGroupId = $"{DefaultValues.PackageGroupPrefix}.{SwixPackageId}"; Log = log; } /// - /// Extracts the contents of the package based on + /// Extracts the contents of the package. /// public void Extract() { @@ -249,6 +266,77 @@ public virtual void Extract(IEnumerable exclusionPatterns) HasBeenExtracted = true; } } + + /// + /// Converts a string containing an SDK version to a semantic version that normalizes the patch level and + /// optionally includes the first two prerelease labels. For example, if the specified version is 6.0.105, then + /// 6.0.100 would be returned. If the version is 6.0.301-preview.2.1234, the result would be 6.0.300-preview.1. + /// + /// A string containing an SDK version. + /// An SDK feature band version. + internal static ReleaseVersion GetSdkFeatureBandVersion(string sdkVersion) + { + ReleaseVersion version = new(sdkVersion); + + // Ignore CI and dev builds. + if (string.IsNullOrEmpty(version.Prerelease) || version.Prerelease.Split('.').Any(s => string.Equals("ci", s) || string.Equals("dev", s))) + { + return new ReleaseVersion(version.Major, version.Minor, version.SdkFeatureBand); + } + + string[] preleaseParts = version.Prerelease.Split('.'); + + // Only the first two prerelease identifiers are used to support side-by-side previews. + string prerelease = (preleaseParts.Length > 1) ? + $"{preleaseParts[0]}.{preleaseParts[1]}" : + preleaseParts[0]; + + return new ReleaseVersion(version.Major, version.Minor, version.SdkFeatureBand, prerelease); + } + + /// + /// Extracts the SDK version from the package ID. + /// + /// The package ID from which to extract the SDK version. + /// A string used to determine where the SDK version should start. + /// SDK version part of the package ID. + /// + internal static string GetSdkVersion(string packageId, string separator) => + !string.IsNullOrWhiteSpace(packageId) && packageId.IndexOf(separator) > -1 ? + packageId.Substring(packageId.IndexOf(separator) + separator.Length) : + throw new FormatException(string.Format(Strings.CannotExtractSdkVersionFromPackageId, packageId)); + + /// + /// Gets the MSI ProductVersion to use for the given packagage. The task item metadata is used first, before falling + /// back to using the version parameter on the task. If neither exist an exception is thrown. + /// + /// The package item to convert into an MSI. + /// The default MSI version 1 + /// + /// + /// + /// + /// + internal static Version GetMsiVersion(ITaskItem package, Version msiVersion, string taskName, + string taskParameterName, string taskItemName) + { + if (!string.IsNullOrWhiteSpace(package.GetMetadata(Metadata.MsiVersion))) + { + // We prefer version metadata information on the package item. + return new(package.GetMetadata(Metadata.MsiVersion)); + } + else if (msiVersion != null) + { + // Fall back to the version provided by the task parameter. + return msiVersion; + } + + // While we could use the major.minor.patch part of the package, it won't work for upgradable MSIs (manifests) and + // unlike packs, we want users to be explicit about the MSI versionsand + // the user to be aware of this and explicitly tell us the value. + throw new Exception(string.Format(Strings.NoInstallerVersion, taskName, + taskParameterName, taskItemName, Metadata.MsiVersion)); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadSetPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadSetPackage.wix.cs new file mode 100644 index 00000000000..37eb9e77ced --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadSetPackage.wix.cs @@ -0,0 +1,105 @@ +// 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; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a NuGet package for a .NET workload set. + /// + internal class WorkloadSetPackage : WorkloadPackageBase + { + /// + /// Prefix separating the SDK feature band within the package ID. + /// + internal const string SdkFeatureBandSeparator = "Microsoft.NET.Workloads."; + + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; + + public override Version MsiVersion + { + get; + } + + /// + /// The SDK feature band version associated with this workload set package. + /// + public ReleaseVersion SdkFeatureBand + { + get; + } + + public WorkloadSetPackage(ITaskItem package, string destinationBaseDirectory, Version msiVersion, + ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(package.ItemSpec, destinationBaseDirectory, shortNames, log) + { + MsiVersion = GetMsiVersion(package, msiVersion, nameof(CreateVisualStudioWorkloadSet), + nameof(CreateVisualStudioWorkloadSet.WorkloadSetMsiVersion), nameof(CreateVisualStudioWorkloadSet.WorkloadSetPackageFiles)); + MsiUtils.ValidateProductVersion(MsiVersion); + SdkFeatureBand = GetSdkFeatureBandVersion(GetSdkVersion(Id)); + SwixPackageGroupId = $"{DefaultValues.PackageGroupPrefix}.NET.Workloads-{SdkFeatureBand.ToString(3)}"; + } + + /// + /// Extracts the SDK version from the package ID. + /// + /// The package ID from which to extract the SDK version. + /// SDK version part of the package ID. + /// + internal static string GetSdkVersion(string packageId) => + GetSdkVersion(packageId, SdkFeatureBandSeparator); + + public override void Extract(IEnumerable exclusionPatterns) + { + base.Extract(exclusionPatterns); + + // We only care about *.workloadset.json files. Directory harvesting doesn't support + // globbing, so we'll delete anything we don't need and log warnings because it could indicate + // a problem with the generated package. + string dataDirectory = Path.Combine(DestinationDirectory, "data"); + + if (!Directory.Exists(dataDirectory)) + { + throw new Exception(string.Format(Strings.InvalidWorkloadSetPackageMissingDataDir, Id)); + } + + // Delete any sub-folders inside the data directory. + foreach (var dir in Directory.EnumerateDirectories(dataDirectory)) + { + Directory.Delete(dir, recursive: true); + } + + bool hasWorkloadSetFiles = false; + + // Remove anything that is not a workload set file. + foreach (var file in Directory.EnumerateFiles(dataDirectory)) + { + if (!Path.GetFileName(file).EndsWith("workloadset.json")) + { + Log?.LogWarning(string.Format(Strings.WarnNonWorkloadSetFileFound, Path.GetFileName(file))); + File.Delete(file); + continue; + } + + hasWorkloadSetFiles = true; + } + + // Fail if there are no workload set files present + if (!hasWorkloadSetFiles) + { + throw new Exception(string.Format(Strings.InvalidWorkloadSetPackageNoWorkloadSet, Id)); + } + } + } +} + +#nullable disable