From 7b7e49b9bf1c3420037983cb6f81e123c65824fb Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Mon, 28 Mar 2022 11:24:35 -0700 Subject: [PATCH 1/6] Refactoring workload build tasks (#8645) * Refactoring workload build tasks * Fix source build and some random cleanup * Updating tests, code cleanup * Minor fixes, unit test conversion * Mark tests as Windows only, fix missing content for Helix * Hide WiX and test packages from Solution Explorer * Fix duplicate publish items * Fix link target for helix * Fix link metadata for WiX * Pass ICE suppressions to Light, more cleanup * Fix file extraction for packs, add unit test for template pack MSI --- .../GenerateManifestMsiTests.cs | 72 --- .../GenerateVisualStudioWorkloadTests.cs | 209 -------- .../GenerateWorkloadMsisTests.cs | 34 -- ....DotNet.Build.Tasks.Workloads.Tests.csproj | 21 +- .../MsiTests.cs | 82 +++ .../PackageTests.cs | 34 ++ .../SwixComponentTests.cs | 125 +++++ ...endencyTests.cs => SwixDependencyTests.cs} | 13 +- .../SwixPackageTests.cs | 34 ++ .../TestBase.cs | 21 + .../VisualStudioComponentTests.cs | 108 ---- .../src/BuildData.wix.cs | 33 ++ .../src/CompileToolTask.cs | 60 --- .../src/CreateVisualStudioWorkload.wix.cs | 330 ++++++++++++ .../src/DefaultValues.cs | 22 + .../src/EmbeddedTemplates.cs | 74 +-- .../src/FileRow.wix.cs | 100 ---- .../src/FrameworkPackPackage.wix.cs | 27 + .../src/GenerateManifestMsi.wix.cs | 479 ------------------ .../src/GenerateMsi.wix.cs | 81 --- .../src/GenerateMsiBase.wix.cs | 478 ----------------- .../src/GenerateTaskBase.cs | 83 --- .../src/GenerateVisualStudioManifest.cs | 91 ---- ...nerateVisualStudioMsiPackageProject.wix.cs | 149 ------ .../src/GenerateVisualStudioWorkload.wix.cs | 312 ------------ .../src/GenerateWorkloadMsis.wix.cs | 210 -------- .../src/GetWorkloadPackPackageReferences.cs | 142 ------ .../src/ICollectionExtensions.cs | 33 -- .../src/LibraryPackPackage.wix.cs | 29 ++ .../src/Metadata.cs | 42 +- ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 21 +- .../src/Misc/msi.csproj | 42 ++ .../src/Msi/FileRow.wix.cs | 105 ++++ .../src/Msi/MsiBase.wix.cs | 222 ++++++++ .../src/Msi/MsiPayloadPackageProject.wix.cs | 59 +++ .../src/Msi/MsiProperties.wix.cs | 121 +++++ .../src/Msi/MsiProperty.wix.cs | 17 + .../src/Msi/MsiUtils.wix.cs | 133 +++++ .../src/Msi/PayloadPackageTokens.wix.cs | 22 + .../src/{ => Msi}/RelatedProduct.wix.cs | 26 +- .../src/Msi/WorkloadManifestMsi.wix.cs | 93 ++++ .../src/Msi/WorkloadPackMsi.wix.cs | 100 ++++ .../src/MsiPackage.cs | 60 --- .../src/MsiProperties.wix.cs | 58 --- .../src/MsiTemplate/ManifestProduct.wxs | 4 +- .../src/MsiTemplate/Product.wxs | 4 +- .../src/MsiUtils.wix.cs | 80 --- .../src/NuGetPackage.cs | 143 ------ .../src/PackageExtractionMethod.cs | 23 + .../src/ProjectTemplateBase.cs | 76 +++ .../src/SdkPackPackage.wix.cs | 28 + .../src/StringExtensions.cs | 23 +- .../src/Strings.Designer.cs | 234 +++++++++ .../src/Strings.resx | 177 +++++++ .../src/Swix/ComponentSwixProject.cs | 77 +++ .../src/Swix/MsiSwixProject.wix.cs | 72 +++ .../src/Swix/SwixComponent.cs | 214 ++++++++ .../src/Swix/SwixDependency.cs | 90 ++++ .../src/Swix/SwixProjectBase.cs | 86 ++++ .../src/Swix/SwixTokens.cs | 23 + .../src/SwixTemplate/msi.swr | 2 +- .../src/TemplatePackPackage.wix.cs | 27 + .../src/ToolsPackPackage.wix.cs | 27 + .../src/Utils.cs | 40 +- .../src/VisualStudioComponent.cs | 297 ----------- .../src/VisualStudioDependency.cs | 70 --- .../{ => Wix}/CommandLineBuilderExtensions.cs | 2 +- .../src/Wix/CompilerToolTask.cs | 78 +++ .../src/{ => Wix}/GuidOptions.cs | 3 +- .../HarvesterToolTask.cs} | 24 +- .../src/{ => Wix}/HeatSuppressions.cs | 7 +- .../LinkerToolTask.cs} | 45 +- .../src/Wix/PreprocessorDefinitionNames.cs | 27 + .../src/Wix/WixExtensions.cs | 27 + .../src/Wix/WixToolTaskBase.cs | 80 +++ .../src/WixToolTask.cs | 52 -- .../src/WorkloadManifestPackage.wix.cs | 175 +++++++ .../src/WorkloadPackPackage.wix.cs | 112 ++++ .../src/WorkloadPackageBase.cs | 251 +++++++++ 79 files changed, 3796 insertions(+), 3511 deletions(-) delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateManifestMsiTests.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateVisualStudioWorkloadTests.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateWorkloadMsisTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs rename src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/{VisualStudioDependencyTests.cs => SwixDependencyTests.cs} (69%) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioComponentTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/CompileToolTask.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/FileRow.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/FrameworkPackPackage.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsi.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsiBase.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateTaskBase.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioManifest.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioMsiPackageProject.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioWorkload.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateWorkloadMsis.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/GetWorkloadPackPackageReferences.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/ICollectionExtensions.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/LibraryPackPackage.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperties.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperty.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{ => Msi}/RelatedProduct.wix.cs (64%) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiPackage.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiProperties.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiUtils.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/NuGetPackage.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/PackageExtractionMethod.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/SdkPackPackage.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixDependency.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixTokens.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/TemplatePackPackage.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsPackPackage.wix.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioComponent.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioDependency.cs rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{ => Wix}/CommandLineBuilderExtensions.cs (96%) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{ => Wix}/GuidOptions.cs (91%) rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{HarvestToolTask.cs => Wix/HarvesterToolTask.cs} (85%) rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{ => Wix}/HeatSuppressions.cs (95%) rename src/Microsoft.DotNet.Build.Tasks.Workloads/src/{LinkToolTask.cs => Wix/LinkerToolTask.cs} (51%) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixExtensions.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/WixToolTask.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateManifestMsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateManifestMsiTests.cs deleted file mode 100644 index 08f7e466972..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateManifestMsiTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Deployment.DotNet.Releases; -using Xunit; - -namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests -{ - public class GenerateManifestMsiTests - { - [WindowsOnlyFact] - public void ItThrowsIfPayloadRelativePathIsTooLong() - { - var task = new GenerateManifestMsi(); - task.MsiVersion = "1.2.3.11111"; - - Exception e = Assert.Throws(() => task.GenerateSwixPackageAuthoring(@"C:\Foo\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi", - "Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100", "x64")); - Assert.Equal(@"Payload relative path exceeds max length (182): Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100,version=1.2.3,chip=x64,productarch=neutral\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi", e.Message); - } - - [WindowsOnlyTheory] - [InlineData("a.b.c", "6.0.100", "x86", "{1c857c83-6584-3c77-ab20-7c77f4fdc097}")] - [InlineData("a.b.c", "6.0.105", "x86", "{1c857c83-6584-3c77-ab20-7c77f4fdc097}")] - [InlineData("a.b.c", "7.0.105-preview.3.5.6.7.8.9", "x86", "{ca8c5ba1-6937-3237-99d5-cbae7b2b8868}")] - [InlineData("a.b.c", "7.0.105-preview.3.5.6.7.8.9", "x64", "{da611d31-8a65-3eea-8949-13bcf2a99950}")] - [InlineData("a.b.c", "7.0.105-preview.4.5.6.7.8.9", "x86", "{e6c1ec79-16d9-3cef-ad7b-0bcefa9636b1}")] - public void ItGeneratesStableUpgradeCodes(string manifestId, string sdkVersion, string platform, string expectedUpgradeCode) - { - ReleaseVersion sdkFeatureBandVersion = GenerateManifestMsi.GetSdkFeatureBandVersion(sdkVersion); - Guid upgradeCode = GenerateManifestMsi.GenerateUpgradeCode(manifestId, sdkFeatureBandVersion, platform); - - Assert.Equal(expectedUpgradeCode.ToLowerInvariant(), upgradeCode.ToString("B")); - } - - [WindowsOnlyTheory] - [InlineData("6.0.100", "6.0.100")] - [InlineData("6.0.103", "6.0.100")] - [InlineData("6.0.103-ci", "6.0.100")] - [InlineData("7.0.1243-preview.8+12345", "7.0.1200-preview.8")] - public void ItIncludesPrereleasePartsInFeatureBandVersion(string sdkVersion, string expectedFeatureBandVersion) - { - ReleaseVersion actual = GenerateManifestMsi.GetSdkFeatureBandVersion(sdkVersion); - ReleaseVersion expected = new ReleaseVersion(expectedFeatureBandVersion); - - Assert.Equal(expected, actual); - } - - [WindowsOnlyTheory] - [InlineData("Microsoft.NET.Workload.Emscripten.Manifest-6.0.100", "6.0.100")] - [InlineData("Microsoft.NET.Workload.Emscripten-7.0.100", null)] - [InlineData(null, null)] - [InlineData("Microsoft.NET.Workload.Emscripten.Manifest-7.3.504-preview.9", "7.3.504-preview.9")] - public void ItCanExtractTheSdkVersionFromTheManifestPackageId(string packageId, string expectedSdkVersion) - { - string actual = GenerateManifestMsi.GetSdkVersionFromPackageId(packageId); - - Assert.Equal(expectedSdkVersion, actual); - } - - [WindowsOnlyTheory] - [InlineData("Microsoft.NET.Workload.Emscripten.Manifest-6.0.100", "Microsoft.NET.Workload.Emscripten")] - [InlineData("Microsoft.NET.Workload.Emscripten-6.0.100", null)] - public void ItCanExtractTheManifestIdFromTheManifestPackageId(string packageId, string expectedManifestId) - { - string actual = GenerateManifestMsi.GetManifestIdFromPackageId(packageId); - - Assert.Equal(expectedManifestId, actual); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateVisualStudioWorkloadTests.cs deleted file mode 100644 index db0628ae384..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateVisualStudioWorkloadTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Arcade.Test.Common; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Xunit; - -namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests -{ - public class GenerateVisualStudioWorkloadTests - { - public string IntermediateBaseOutputPath = Path.Combine(AppContext.BaseDirectory, "obj"); - - public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); - - public string TestIntermediateBaseOutputPath => Path.Combine(IntermediateBaseOutputPath, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); - - [Fact] - public void ItIgnoresNotApplicableAliasedPacks() - { - string workloadManifest = Path.Combine(AppContext.BaseDirectory, "testassets", "AbstractWorkloadsNonWindowsPacks.json"); - - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadManifests = new TaskItem[] - { - new TaskItem(workloadManifest) - }, - ComponentVersions = new TaskItem[] - { - new TaskItem("microsoft-net-runtime-ios", new Dictionary { { "Version", "6.5.38766" } }), - new TaskItem("runtimes-ios", new Dictionary { { "Version", "6.5.38766" } }), - new TaskItem("microsoft-net-runtime-mono-tooling", new Dictionary { { "Version", "6.5.38766" } }), - }, - GenerateMsis = false, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - BuildEngine = new MockBuildEngine() - }; - - Assert.True(buildTask.Execute()); - string outputPath = Path.GetDirectoryName(buildTask.SwixProjects[0].GetMetadata("FullPath")); - string componentSwr = File.ReadAllText(Path.Combine(outputPath, "component.swr")); - - Assert.Contains(@"package name=microsoft.net.runtime.ios", componentSwr); - Assert.DoesNotContain(@"vs.dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm", componentSwr); - Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm64", componentSwr); - Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-arm64", componentSwr); - Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x64", componentSwr); - Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x86", componentSwr); - Assert.Contains(@"vs.dependency id=runtimes.ios", componentSwr); - } - - [Fact] - public void ItGeneratesASwixProjectFromAWorkloadManifest() - { - string workloadManifest = Path.Combine(AppContext.BaseDirectory, "testassets", "WorkloadManifest.json"); - - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadManifests = new TaskItem[] - { - new TaskItem(workloadManifest) - }, - ComponentVersions = new TaskItem[] - { - new TaskItem("microsoft-net-sdk-blazorwebassembly-aot", new Dictionary { { "Version", "6.5.38766" } }), - }, - GenerateMsis = false, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - BuildEngine = new MockBuildEngine() - }; - - Assert.True(buildTask.Execute()); - string outputPath = Path.GetDirectoryName(buildTask.SwixProjects[0].GetMetadata("FullPath")); - string componentSwr = File.ReadAllText(Path.Combine(outputPath, "component.swr")); - - Assert.Single(buildTask.SwixProjects); - Assert.Contains(@"package name=microsoft.net.sdk.blazorwebassembly.aot - version=6.5.38766", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.NET.Runtime.MonoAOTCompiler.Task.6.0.0-preview.4.21201.1", componentSwr); - } - - [Fact] - public void ItCanShortenPackageIds() - { - string workloadManifest = Path.Combine(AppContext.BaseDirectory, "testassets", "WorkloadManifest.json"); - - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadManifests = new TaskItem[] - { - new TaskItem(workloadManifest) - }, - ShortNames = new TaskItem[] - { - new TaskItem("Microsoft.NET.Runtime", new Dictionary { {"Replacement", "MSFT"} }) - }, - ComponentVersions = new TaskItem[] - { - new TaskItem("microsoft-net-sdk-blazorwebassembly-aot", new Dictionary { { "Version", "6.5.38766" } }), - }, - GenerateMsis = false, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - BuildEngine = new MockBuildEngine() - }; - - Assert.True(buildTask.Execute()); - string outputPath = Path.GetDirectoryName(buildTask.SwixProjects[0].GetMetadata("FullPath")); - string componentSwr = File.ReadAllText(Path.Combine(outputPath, "component.swr")); - - Assert.Single(buildTask.SwixProjects); - Assert.Contains(@"package name=microsoft.net.sdk.blazorwebassembly.aot - version=6.5.38766", componentSwr); - Assert.Contains("vs.dependency id=MSFT.MonoAOTCompiler.Task.6.0.0-preview.4.21201.1", componentSwr); - } - - [Fact] - public void ItGeneratesASwixProjectFromAWorkloadManifestPackage() - { - string workloadPackage = Path.Combine(AppContext.BaseDirectory, "testassets", - "microsoft.net.sdk.blazorwebassembly.aot.6.0.0-preview.4.21209.5.nupkg"); - - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadPackages = new TaskItem[] - { - new TaskItem(workloadPackage) - }, - - GenerateMsis = false, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - BuildEngine = new MockBuildEngine() - }; - - Assert.True(buildTask.Execute()); - string outputPath = Path.GetDirectoryName(buildTask.SwixProjects[0].GetMetadata("FullPath")); - string componentSwr = File.ReadAllText(Path.Combine(outputPath, "component.swr")); - - Assert.Single(buildTask.SwixProjects); - Assert.Contains(@"package name=microsoft.net.sdk.blazorwebassembly.aot - version=1.0", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.NET.Runtime.MonoAOTCompiler.Task.6.0.0-preview.4.21201.1", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.NET.Runtime.Emscripten.Python.6.0.0-preview.4.21205.1", componentSwr); - } - - [Fact] - public void ItIncludesAbstractManifests() - { - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadManifests = new TaskItem[] - { - new TaskItem(Path.Combine(TestAssetsPath, "BlazorWorkloadManifest.json")) - }, - GenerateMsis = false, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - BuildEngine = new MockBuildEngine() - }; - - Assert.True(buildTask.Execute()); - string blazorOutputPath = Path.GetDirectoryName(buildTask.SwixProjects[0].GetMetadata("FullPath")); - string blazorComponentSwr = File.ReadAllText(Path.Combine(blazorOutputPath, "component.swr")); - Assert.Contains(@"package name=microsoft.net.sdk.blazorwebassembly.aot - version=6.0.0.0", blazorComponentSwr); - string androidOutputPath = Path.GetDirectoryName(buildTask.SwixProjects[1].GetMetadata("FullPath")); - string androidComponentSwr = File.ReadAllText(Path.Combine(androidOutputPath, "component.swr")); - Assert.Contains(@"package name=microsoft.net.runtime.android - version=6.0.0.0", androidComponentSwr); - } - - [Fact] - public void ItReportsMissingPacks() - { - var buildTask = new GenerateVisualStudioWorkload() - { - WorkloadManifests = new TaskItem[] - { - new TaskItem(Path.Combine(TestAssetsPath, "BlazorWorkloadManifest.json")) - }, - GenerateMsis = true, - IntermediateBaseOutputPath = TestIntermediateBaseOutputPath, - WixToolsetPath = "", - PackagesPath = Path.Combine(TestAssetsPath, "packages"), - BuildEngine = new MockBuildEngine() - }; - - // The task will fail to generate VS components because we have no generated MSI packages. - // The package feeds are volatile until we actually release and the execution time for the unit tests would spike - Assert.False(buildTask.Execute()); - ITaskItem missingPack = buildTask.MissingPacks.Where(mp => string.Equals(mp.ItemSpec, "Microsoft.NET.Runtime.MonoAOTCompiler.Task")).FirstOrDefault(); - - // This package would be required by the workload, but would be missing - Assert.Equal(Path.Combine(TestAssetsPath, "packages", "Microsoft.NET.Runtime.MonoAOTCompiler.Task.6.0.0-preview.5.21262.5.nupkg"), missingPack.GetMetadata("SourcePackage")); - - // This package should not show as missing because it has the wrong platform and belongs to an abstract workload - Assert.DoesNotContain("Microsoft.NETCore.App.Runtime.AOT.osx-x64.Cross.ios-arm", buildTask.MissingPacks.Select(p => p.ItemSpec)); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateWorkloadMsisTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateWorkloadMsisTests.cs deleted file mode 100644 index c42c81ffa56..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/GenerateWorkloadMsisTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using System.Linq; -using Microsoft.Build.Utilities; -using Xunit; - -namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests -{ - public class GenerateWorkloadMsisTests - { - public string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); - - [Fact] - public void ItRemovesDuplicateWorkloadPacks() - { - TaskItem[] manifests = new[] - { - new TaskItem(Path.Combine(TestAssetsPath, "emsdkWorkloadManifest.json")), - new TaskItem(Path.Combine(TestAssetsPath, "emsdkWorkloadManifest2.json")), - }; - - var packs = GenerateWorkloadMsis.GetWorkloadPacks(manifests).ToArray(); - - Assert.Equal(4, packs.Length); - Assert.Equal("Microsoft.NET.Runtime.Emscripten.Node", packs[0].Id.ToString()); - Assert.Equal("7.0.0-alpha.2.22078.1", packs[0].Version); - Assert.Equal("Microsoft.NET.Runtime.Emscripten.Node", packs[3].Id.ToString()); - Assert.Equal("7.0.0-alpha.2.22079.1", packs[3].Version); - } - } -} 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 5fe0d0a0bf8..b9c895b06cd 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 @@ -1,4 +1,4 @@ - + net472;$(TargetFrameworkForNETSDK) @@ -14,11 +14,18 @@ - + + + + + - + + + + @@ -26,4 +33,12 @@ + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs new file mode 100644 index 00000000000..e6151d9dde6 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using Microsoft.Arcade.Test.Common; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.WindowsInstaller; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + [Collection("6.0.200 Toolchain manifest tests")] + public class MsiTests : TestBase + { + [WindowsOnlyFact] + public void ItCanBuildAManifestMsi() + { + string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); + TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + WorkloadManifestPackage pkg = new(packageItem, PackageRootDirectory, new Version("1.2.3")); + pkg.Extract(); + WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); + + ITaskItem item = msi.Build(MsiOutputPath); + + string msiPath = item.GetMetadata(Metadata.FullPath); + + // Process the summary information stream's template to extract the MSIs target platform. + using SummaryInfo si = new(msiPath, enableWrite: false); + + // UpgradeCode is predictable/stable for manifest MSIs. + Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); + Assert.Equal("1.2.3", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion)); + Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", MsiUtils.GetProviderKeyName(msiPath)); + Assert.Equal("x64;1033", si.Template); + + // Generated MSI should return the path where the .wixobj files are located so + // WiX packs can be created for post-build signing. + Assert.NotNull(item.GetMetadata(Metadata.WixObj)); + } + + [WindowsOnlyFact] + public void ItCanBuildATemplatePackMsi() + { + string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); + string packagePath = Path.Combine(TestAssetsPath, "microsoft.ios.templates.15.2.302-preview.14.122.nupkg"); + + WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null); + TemplatePackPackage pkg = new(p, packagePath, new[] {"x64"}, PackageRootDirectory); + pkg.Extract(); + WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); + + ITaskItem item = msi.Build(MsiOutputPath); + + string msiPath = item.GetMetadata(Metadata.FullPath); + + // Process the summary information stream's template to extract the MSIs target platform. + using SummaryInfo si = new(msiPath, enableWrite: false); + + // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). + Assert.Equal("{EC4D6B34-C9DE-3984-97FD-B7AC96FA536A}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); + // The version is set using the package major.minor.patch + Assert.Equal("15.2.302.0", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion)); + Assert.Equal("Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", MsiUtils.GetProviderKeyName(msiPath)); + Assert.Equal("x64;1033", si.Template); + + // Template packs should pull in the raw nupkg. We can verify by query the File table. There should + // only be a single file. + FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault(); + Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupk", fileRow.FileName); + + // Generated MSI should return the path where the .wixobj files are located so + // WiX packs can be created for post-build signing. + Assert.NotNull(item.GetMetadata(Metadata.WixObj)); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs new file mode 100644 index 00000000000..579a0e66a28 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/PackageTests.cs @@ -0,0 +1,34 @@ +// 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 Xunit; +using Microsoft.DotNet.Build.Tasks.Workloads; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; +using System.IO; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + [Collection("6.0.200 Toolchain manifest tests")] + public class PackageTests : TestBase + { + [WindowsOnlyFact] + 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")); + WorkloadManifestPackage p = new(manifestPackageItem, PackageRootDirectory, new Version("1.2.3")); + + ReleaseVersion expectedFeatureBand = new("6.0.200"); + + Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain", p.ManifestId); + Assert.Equal(expectedFeatureBand, p.SdkFeatureBand); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs new file mode 100644 index 00000000000..3e7fb8ea965 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs @@ -0,0 +1,125 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Build.Tasks.Workloads.Swix; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + public class SwixComponentTests : TestBase + { + public string RandomPath => Path.Combine(AppContext.BaseDirectory, "obj", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + + [WindowsOnlyFact] + public void ItAssignsDefaultValues() + { + WorkloadManifest manifest = Create("WorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest); + + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr); + + string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr")); + Assert.Contains(@"title=""Blazor WebAssembly AOT workload""", componentResSwr); + Assert.Contains(@"description=""Blazor WebAssembly AOT workload""", componentResSwr); + Assert.Contains(@"category="".NET""", componentResSwr); + } + + [WindowsOnlyFact] + public void ItShortensComponentIds() + { + ITaskItem[] shortNames = new TaskItem[] + { + new TaskItem("Microsoft.NET.Runtime", new Dictionary { { Metadata.Replacement, "MSFT" } }) + }; + + WorkloadManifest manifest = Create("WorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, shortNames: shortNames); + + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + Assert.Contains("vs.dependency id=MSFT.MonoAOTCompiler.Task.6.0.0-preview.4.21201.1", componentSwr); + } + + [WindowsOnlyFact] + public void ItIgnoresNonApplicableDepedencies() + { + WorkloadManifest manifest = Create("AbstractWorkloadsNonWindowsPacks.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, null, null); + + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + Assert.Contains(@"package name=microsoft.net.runtime.ios", componentSwr); + Assert.DoesNotContain(@"vs.dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm", componentSwr); + Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm64", componentSwr); + Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-arm64", componentSwr); + Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x64", componentSwr); + Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x86", componentSwr); + Assert.Contains(@"vs.dependency id=runtimes.ios", componentSwr); + } + + [WindowsOnlyFact] + public void ItCanOverrideDefaultValues() + { + WorkloadManifest manifest = Create("WorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + + ITaskItem[] componentResources = new ITaskItem[] + { + new TaskItem("microsoft-net-sdk-blazorwebassembly-aot", new Dictionary { + { "Title", "AOT" }, + { "Description", "A long wordy description." }, + { "Category", "Compilers, build tools, and runtimes" } + }) + }; + + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, componentResources); + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr")); + + Assert.Contains(@"title=""AOT""", componentResSwr); + Assert.Contains(@"description=""A long wordy description.""", componentResSwr); + Assert.Contains(@"category=""Compilers, build tools, and runtimes""", componentResSwr); + } + + [Fact] + public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks() + { + WorkloadManifest manifest = Create("mauiWorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest); + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + Assert.Contains(@"vs.dependency id=maui.mobile", componentSwr); + Assert.Contains(@"vs.dependency id=maui.desktop", componentSwr); + } + + private static WorkloadManifest Create(string filename) + { + return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(filename), + File.OpenRead(Path.Combine(TestAssetsPath, filename))); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioDependencyTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixDependencyTests.cs similarity index 69% rename from src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioDependencyTests.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixDependencyTests.cs index be810f26e5b..54113fb168a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioDependencyTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixDependencyTests.cs @@ -2,28 +2,25 @@ // 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; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { - public class VisualStudioDependencyTests + public class SwixDependencyTests { - [Theory] + [WindowsOnlyTheory] [InlineData("1.0.0", null, "[1.0.0,)")] [InlineData("1.0.0", "2.0.0", "[1.0.0,2.0.0)")] [InlineData("1.0.0", "1.0.0", "[1.0.0]")] + [InlineData(null, "1.2.3", "[,1.2.3)")] public void ItGeneratesVersionRanges(string minVersion, string maxVersion, string expectedVersionRange) { Version v1 = string.IsNullOrWhiteSpace(minVersion) ? null : new Version(minVersion); Version v2 = string.IsNullOrWhiteSpace(maxVersion) ? null : new Version(maxVersion); - VisualStudioDependency dep = new VisualStudioDependency("foo", v1, v2); + SwixDependency dep = new("foo", v1, v2); - Assert.Equal(expectedVersionRange, dep.GetVersion()); + Assert.Equal(expectedVersionRange, dep.GetVersionRange()); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs new file mode 100644 index 00000000000..af08ce74b72 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs @@ -0,0 +1,34 @@ +// 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.Build.Utilities; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Swix; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + public class SwixPackageTests : TestBase + { + [WindowsOnlyFact] + public void ItThrowsIfPackageRelativePathExceedsLimit() + { + TaskItem msiItem = new TaskItem("Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi"); + msiItem.SetMetadata(Metadata.SwixPackageId, "Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100"); + msiItem.SetMetadata(Metadata.Version, "6.0.0.0"); + msiItem.SetMetadata(Metadata.Platform, "x64"); + + Exception e = Assert.Throws(() => + { + MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath); + }); + + 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\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi.", e.Message); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs new file mode 100644 index 00000000000..52fb73d78bf --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + public abstract class TestBase + { + public static readonly string BaseIntermediateOutputPath = Path.Combine(AppContext.BaseDirectory, "obj", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + public static readonly string BaseOutputPath = Path.Combine(AppContext.BaseDirectory, "bin", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + + public static readonly string MsiOutputPath = Path.Combine(BaseOutputPath, "msi"); + public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); + + public static readonly string WixToolsetPath = Path.Combine(TestAssetsPath, "wix"); + + public static readonly string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioComponentTests.cs deleted file mode 100644 index 7d0afdcd436..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/VisualStudioComponentTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; -using Xunit; - -namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests -{ - public class VisualStudioComponentTests - { - public static readonly ITaskItem[] NoItems = Array.Empty(); - - public string RandomPath => Path.Combine(AppContext.BaseDirectory, "obj", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); - - [Fact] - public void ItAssignsDefaultValues() - { - WorkloadManifest manifest = Create("WorkloadManifest.json"); - WorkloadDefinition definition = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - VisualStudioComponent component = VisualStudioComponent.Create(null, manifest, definition, NoItems, NoItems, NoItems, NoItems); - - string swixProjDirectory = RandomPath; - Directory.CreateDirectory(swixProjDirectory); - component.Generate(swixProjDirectory); - - string componentResSwr = File.ReadAllText(Path.Combine(swixProjDirectory, "component.res.swr")); - - Assert.Contains(@"title=""Blazor WebAssembly AOT workload""", componentResSwr); - Assert.Contains(@"description=""Blazor WebAssembly AOT workload""", componentResSwr); - Assert.Contains(@"category="".NET""", componentResSwr); - } - - [Fact] - public void ItCanOverrideDefaultValues() - { - WorkloadManifest manifest = Create("WorkloadManifest.json"); - WorkloadDefinition definition = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - - ITaskItem[] resources = new ITaskItem[] - { - new TaskItem("microsoft-net-sdk-blazorwebassembly-aot", new Dictionary { - { "Title", "AOT" }, - { "Description", "A long wordy description." }, - { "Category", "Compilers, build tools, and runtimes" } - }) - }; - - VisualStudioComponent component = VisualStudioComponent.Create(null, manifest, definition, NoItems, NoItems, resources, NoItems); - - string swixProjDirectory = RandomPath; - Directory.CreateDirectory(swixProjDirectory); - component.Generate(swixProjDirectory); - - string componentResSwr = File.ReadAllText(Path.Combine(swixProjDirectory, "component.res.swr")); - - Assert.Contains(@"title=""AOT""", componentResSwr); - Assert.Contains(@"description=""A long wordy description.""", componentResSwr); - Assert.Contains(@"category=""Compilers, build tools, and runtimes""", componentResSwr); - } - - [Fact] - public void ItCreatesSafeComponentIds() - { - WorkloadManifest manifest = Create("WorkloadManifest.json"); - WorkloadDefinition definition = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - VisualStudioComponent component = VisualStudioComponent.Create(null, manifest, definition, NoItems, NoItems, NoItems, NoItems); - - string swixProjDirectory = RandomPath; - Directory.CreateDirectory(swixProjDirectory); - component.Generate(swixProjDirectory); - - string componentSwr = File.ReadAllText(Path.Combine(swixProjDirectory, "component.swr")); - - Assert.Contains(@"microsoft.net.sdk.blazorwebassembly.aot", componentSwr); - } - - [Fact] - public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks() - { - WorkloadManifest manifest = Create("mauiWorkloadManifest.json"); - WorkloadDefinition definition = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - VisualStudioComponent component = VisualStudioComponent.Create(null, manifest, definition, NoItems, NoItems, NoItems, NoItems); - - string swixProjDirectory = RandomPath; - Directory.CreateDirectory(swixProjDirectory); - component.Generate(swixProjDirectory); - - string componentSwr = File.ReadAllText(Path.Combine(swixProjDirectory, "component.swr")); - - Assert.Contains(@"vs.dependency id=maui.mobile", componentSwr); - Assert.Contains(@"vs.dependency id=maui.desktop", componentSwr); - } - - private static WorkloadManifest Create(string filename) - { - return WorkloadManifestReader.ReadWorkloadManifest( - Path.GetFileNameWithoutExtension(filename), - File.OpenRead(Path.Combine(AppContext.BaseDirectory, "testassets", filename)), - filename); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs new file mode 100644 index 00000000000..3642877491a --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Deployment.DotNet.Releases; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Describes information related to building MSIs for a workload pack that + /// potentially spans multiple feature bands. + /// + internal class BuildData + { + /// + /// The workload pack to use for creating an MSI + /// + public WorkloadPackPackage Package + { + get; + } + + /// + /// The set of feature bands that include contain a reference to this pack. + /// + public Dictionary> FeatureBands = new(); + + public BuildData(WorkloadPackPackage package) + { + Package = package; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CompileToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CompileToolTask.cs deleted file mode 100644 index c875d1854bc..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CompileToolTask.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public class CompileToolTask : WixToolTask - { - /// - /// The default architecture for a package, components, etc. - /// - public string Arch - { - get; - set; - } = "x86"; - - /// - /// A collection of WiX extension assemblies to use. - /// - public ICollection Extensions - { - get; - } = new List(); - - public string OutputPath - { - get; - set; - } - - public IEnumerable SourceFiles - { - get; - set; - } - - protected override string ToolName => "candle.exe"; - - public CompileToolTask(IBuildEngine engine, string wixToolsetPath) : base(engine, wixToolsetPath) - { - - } - - protected override string GenerateCommandLineCommands() - { - CommandLineBuilder.AppendSwitchIfNotNull("-out ", OutputPath); - // No trailing space, preprocessor definitions are passed as -d= - CommandLineBuilder.AppendArrayIfNotNull("-d", PreprocessorDefinitions.ToArray()); - CommandLineBuilder.AppendArrayIfNotNull("-ext ", Extensions.ToArray()); - CommandLineBuilder.AppendSwitchIfNotNull("-arch ", Arch); - CommandLineBuilder.AppendFileNamesIfNotNull(SourceFiles.ToArray(), " "); - return CommandLineBuilder.ToString(); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs new file mode 100644 index 00000000000..ef2fb050577 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -0,0 +1,330 @@ +// 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 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 Parallel = System.Threading.Tasks.Parallel; + +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 + { + /// + /// 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. + /// + public ITaskItem[] ComponentResources + { + get; + set; + } + + /// + /// A set of Internal Consistency Evaluators (ICEs) to suppress. + /// + public ITaskItem[] IceSuppressions + { + get; + set; + } + + /// + /// The version to assign to workload manifest installers. + /// + public Version ManifestMsiVersion + { + 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. + /// + 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. + /// + public ITaskItem[] ShortNames + { + 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; + } + + /// + /// A set of packages containing workload manifests. + /// + [Required] + public ITaskItem[] WorkloadManifestPackageFiles + { + get; + set; + } + + public override bool Execute() + { + try + { + // TODO: trim out duplicate manifests. + List manifestPackages = new(); + List manifestMsisToBuild = new(); + List swixComponents = new(); + Dictionary buildData = 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, ManifestMsiVersion, ShortNames, Log); + manifestPackages.Add(manifestPackage); + + foreach (string platform in SupportedPlatforms) + { + manifestMsisToBuild.Add(new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath)); + } + + // 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(); + + 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) + { + WorkloadPack pack = manifest.Packs[packId]; + + foreach ((string sourcePackage, string[] platforms) in WorkloadPackPackage.GetSourcePackages(PackageSource, pack)) + { + if (!File.Exists(sourcePackage)) + { + throw new FileNotFoundException(message: null, 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)); + } + + 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)) + { + buildData[sourcePackage].FeatureBands[platform] = new(); + } + + _ = buildData[sourcePackage].FeatureBands[platform].Add(manifestPackage.SdkFeatureBand); + } + + } + } + + // Finally, add a component for the workload in Visual Studio. + SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, + ComponentResources, ShortNames); + swixComponents.Add(component); + } + } + } + + List msiItems = new(); + List swixProjectItems = new(); + + _ = 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); + + // Create the JSON manifest for CLI based installations. + string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); + + // Generate a .csproj to package the MSI and its manifest for CLI installs. + MsiPayloadPackageProject csproj = new(msi.Package, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); + + lock (msiItems) + { + msiItems.Add(msiOutputItem); + } + + MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = swixProject.Create(); + + foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) + { + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); + + 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); + + // Create the JSON manifest for CLI based installations. + string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); + + // Generate SWIX authoring for the MSI package. + MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); + ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}"); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } + + // Generate a .csproj to package the MSI and its manifest for CLI installs. + MsiPayloadPackageProject csproj = new(msi.Package, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); + + lock (msiItems) + { + 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 => + { + ComponentSwixProject swixComponentProject = new(swixComponent, BaseIntermediateOutputPath, BaseOutputPath); + ITaskItem swixProjItem = new TaskItem(swixComponentProject.Create()); + swixProjItem.SetMetadata(Metadata.SdkFeatureBand, $"{swixComponent.SdkFeatureBand}"); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjItem); + } + }); + + Msis = msiItems.ToArray(); + SwixProjects = swixProjectItems.ToArray(); + } + catch (Exception e) + { + Log.LogError(e.ToString()); + } + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs new file mode 100644 index 00000000000..4eba3f9c4e5 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Defines default values that can be used to when creating workload artifacts. + /// + internal static class DefaultValues + { + /// + /// The default category to assign to a SWIX component. The value is used + /// to group individual components in Visual Studio Installer. + /// + public static readonly string ComponentCategory = ".NET"; + + /// + /// The default value to assign to the Manufacturer property of an MSI. + /// + public static readonly string Manufacturer = "Microsoft Corporation"; + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index 2fbabf1bcae..536f40f466a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -4,32 +4,40 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.DotNet.Build.Tasks.Workloads { internal class EmbeddedTemplates { - internal static TaskLoggingHelper Log; - - private static readonly string s_namespace = ""; - - private static readonly Dictionary _templateResources = new(); + private static readonly Dictionary s_templateResources = new(); + /// + /// Extracts the specified filename from the embedded template resource into the destination folder and + /// returns the full path of the file. + /// + /// The name of the template file to extract. + /// The directory where the file will be extracted. + /// The full path of the extracted file. public static string Extract(string filename, string destinationFolder) { return Extract(filename, destinationFolder, filename); } + /// + /// Extracts the specified filename from the embedded template resource into the destination folder using + /// the specified filename and return the full path of the file. + /// + /// The name of the template file to extract. + /// The directory where the file will be extracted. + /// The full path of the extracted file. public static string Extract(string filename, string destinationFolder, string destinationFilename) { - if (!_templateResources.TryGetValue(filename, out string resourceName)) + if (!s_templateResources.TryGetValue(filename, out string resourceName)) { - throw new KeyNotFoundException($"No template for '{filename}' exists."); + throw new KeyNotFoundException(string.Format(Strings.TemplateNotFound, filename)); } - // Clean out stale files, just to be safe. + // Clean out stale files just to be safe. string destinationPath = Path.Combine(destinationFolder, destinationFilename); if (File.Exists(destinationPath)) { @@ -43,42 +51,40 @@ public static string Extract(string filename, string destinationFolder, string d using Stream rs = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); using FileStream fs = new(destinationPath, FileMode.Create, FileAccess.Write); - if (rs != null) - { - rs.CopyTo(fs); - Log?.LogMessage(MessageImportance.Low, $"Resource '{resourceName}' extracted to '{destinationPath}"); - } - else + if (rs == null) { - Log?.LogMessage(MessageImportance.Low, $"Unable to find resource: {resourceName}"); + throw new IOException(string.Format(Strings.TemplateResourceNotFound, resourceName)); } + rs.CopyTo(fs); + return destinationPath; } static EmbeddedTemplates() { - s_namespace = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + string ns = MethodBase.GetCurrentMethod().DeclaringType.Namespace; - _templateResources = new() + s_templateResources = new() { - { "DependencyProvider.wxs", $"{s_namespace}.MsiTemplate.DependencyProvider.wxs" }, - { "Directories.wxs", $"{s_namespace}.MsiTemplate.Directories.wxs" }, - { "dotnethome_x64.wxs", $"{s_namespace}.MsiTemplate.dotnethome_x64.wxs" }, - { "ManifestProduct.wxs", $"{s_namespace}.MsiTemplate.ManifestProduct.wxs" }, - { "Product.wxs", $"{s_namespace}.MsiTemplate.Product.wxs" }, - { "Registry.wxs", $"{s_namespace}.MsiTemplate.Registry.wxs" }, - { "Variables.wxi", $"{s_namespace}.MsiTemplate.Variables.wxi" }, + { "DependencyProvider.wxs", $"{ns}.MsiTemplate.DependencyProvider.wxs" }, + { "Directories.wxs", $"{ns}.MsiTemplate.Directories.wxs" }, + { "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" }, + { "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" }, + { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, + { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, + { "Variables.wxi", $"{ns}.MsiTemplate.Variables.wxi" }, - { $"msi.swr", $"{s_namespace}.SwixTemplate.msi.swr" }, - { $"msi.swixproj", $"{s_namespace}.SwixTemplate.msi.swixproj"}, - { $"component.swr", $"{s_namespace}.SwixTemplate.component.swr" }, - { $"component.res.swr", $"{s_namespace}.SwixTemplate.component.res.swr" }, - { $"component.swixproj", $"{s_namespace}.SwixTemplate.component.swixproj" }, - { $"manifest.vsmanproj", $"{s_namespace}.SwixTempalte.manifest.vsmanproj"}, + { $"msi.swr", $"{ns}.SwixTemplate.msi.swr" }, + { $"msi.swixproj", $"{ns}.SwixTemplate.msi.swixproj" }, + { $"component.swr", $"{ns}.SwixTemplate.component.swr" }, + { $"component.res.swr", $"{ns}.SwixTemplate.component.res.swr" }, + { $"component.swixproj", $"{ns}.SwixTemplate.component.swixproj" }, + { $"manifest.vsmanproj", $"{ns}.SwixTempalte.manifest.vsmanproj" }, - { "Icon.png", $"{s_namespace}.Misc.Icon.png"}, - { "LICENSE.TXT", $"{s_namespace}.Misc.LICENSE.TXT"} + { "Icon.png", $"{ns}.Misc.Icon.png" }, + { "LICENSE.TXT", $"{ns}.Misc.LICENSE.TXT" }, + { "msi.csproj", $"{ns}.Misc.msi.csproj" } }; } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FileRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FileRow.wix.cs deleted file mode 100644 index 9cb57237084..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FileRow.wix.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Deployment.WindowsInstaller; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Describes a single row of an MSI File table. - /// - public class FileRow - { - public int Attributes - { - get; - set; - } - - public string Component - { - get; - set; - } - - public string File - { - get; - set; - } - - public string FileName - { - get; - set; - } - - public int FileSize - { - get; - set; - } - - public string Language - { - get; - set; - } - - public string LongFileName - { - get - { - int index = FileName.IndexOf("|"); - - if (index == -1) - { - return FileName; - } - else - { - return FileName.Substring(index + 1); - } - } - } - - public int Sequence - { - get; - set; - } - - public string TargetPath - { - get; - set; - } - - public string Version - { - get; - set; - } - - public static FileRow Create(Record fileRecord, string targetPath) - { - return new FileRow - { - Attributes = (int)fileRecord["Attributes"], - Component = (string)fileRecord["Component_"], - File = (string)fileRecord["File"], - FileName = (string)fileRecord["FileName"], - FileSize = (int)fileRecord["FileSize"], - Language = (string)fileRecord["Language"], - Sequence = (int)fileRecord["Sequence"], - TargetPath = targetPath, - Version = (string)fileRecord["Version"], - }; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FrameworkPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FrameworkPackPackage.wix.cs new file mode 100644 index 00000000000..d4def63d9f8 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/FrameworkPackPackage.wix.cs @@ -0,0 +1,27 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a framework pack. + /// + internal class FrameworkPackPackage : WorkloadPackPackage + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; + + public FrameworkPackPackage(WorkloadPack pack, string packagePath, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(pack, packagePath, platforms, destinationBaseDirectory, shortNames, log) + { } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.wix.cs deleted file mode 100644 index 4f6391f7fc7..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.wix.cs +++ /dev/null @@ -1,479 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Xml; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.Deployment.DotNet.Releases; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public class GenerateManifestMsi : GenerateTaskBase - { - private const string ManifestSeparator = ".Manifest-"; - - private ReleaseVersion _sdkFeaureBandVersion; - - /// - /// Gets or sets whether a corresponding SWIX project should be generated for the MSI. - /// - public bool GenerateSwixAuthoring - { - get; - set; - } = true; - - /// - /// The path where the generated MSIs will be placed. - /// - [Required] - public string OutputPath - { - get; - set; - } - - /// - /// The ID of the workload manifest. - /// - public string ManifestId - { - get; - set; - } - - /// - /// The set of MSIs that were produced. - /// - [Output] - public ITaskItem[] Msis - { - get; - protected set; - } - - private ReleaseVersion SdkFeatureBandVersion - { - get - { - _sdkFeaureBandVersion ??= GetSdkFeatureBandVersion(SdkVersion); - - return _sdkFeaureBandVersion; - } - } - - /// - /// The SDK version, e.g. 6.0.107. - /// - public string SdkVersion - { - get; - set; - } - - /// - /// An item group containing information to shorten the names of packages. - /// - public ITaskItem[] ShortNames - { - get; - set; - } - - /// - /// Semicolon sepearate list of ICEs to suppress. - /// - public string SuppressIces - { - get; - set; - } - - /// - /// The version of the MSI. - /// - [Required] - public string MsiVersion - { - get; - set; - } - - [Required] - public string WorkloadManifestPackage - { - get; - set; - } - - public override bool Execute() - { - try - { - NugetPackage nupkg = new(WorkloadManifestPackage, Log); - List msis = new(); - - // Extract the manifest ID from manifest package ID if no value was provided. - if (string.IsNullOrWhiteSpace(ManifestId)) - { - ManifestId = GetManifestIdFromPackageId(nupkg.Id); - - if (string.IsNullOrWhiteSpace(ManifestId)) - { - Log.LogError($"Unable to parse a manifest ID from package ID: '{nupkg.Id}'. Please provide the 'ManifestId' parameter."); - } - } - - // If no explicit SDK version is provided we extract it from the package ID. Manifest packages - // have a fixed format that must end in the target SDK. - if (string.IsNullOrWhiteSpace(SdkVersion)) - { - SdkVersion = GetSdkVersionFromPackageId(nupkg.Id); - - if (string.IsNullOrWhiteSpace(SdkVersion)) - { - Log.LogError($"Unable to parse the SDK version from package ID: '{nupkg.Id}'. Please provide the 'SdkVersion' parameter."); - } - } - - Log.LogMessage(MessageImportance.High, $"Generating workload manifest installer for {SdkFeatureBandVersion}"); - - // MSI ProductName defaults to the package title and fallback to the package ID with a warning. - string productName = nupkg.Title; - - if (string.IsNullOrWhiteSpace(nupkg.Title)) - { - Log?.LogMessage(MessageImportance.High, $"'{WorkloadManifestPackage}' should have a non-empty title. The MSI ProductName will be set to the package ID instead."); - productName = nupkg.Id; - } - - // Extract once, but harvest multiple times because some generated attributes are platform dependent. - string packageContentsDirectory = Path.Combine(PackageDirectory, $"{nupkg.Identity}"); - nupkg.Extract(packageContentsDirectory, Enumerable.Empty()); - string packageContentsDataDirectory = Path.Combine(packageContentsDirectory, "data"); - - foreach (string platform in GenerateMsiBase.SupportedPlatforms) - { - // Extract the MSI template and add it to the list of source files. - List sourceFiles = new(); - string msiSourcePath = Path.Combine(MsiDirectory, $"{nupkg.Id}", $"{nupkg.Version}", platform); - sourceFiles.Add(EmbeddedTemplates.Extract("DependencyProvider.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("dotnethome_x64.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("ManifestProduct.wxs", msiSourcePath)); - - string EulaRtfPath = Path.Combine(msiSourcePath, "eula.rtf"); - File.WriteAllText(EulaRtfPath, GenerateMsiBase.Eula.Replace("__LICENSE_URL__", nupkg.LicenseUrl)); - EmbeddedTemplates.Extract("Variables.wxi", msiSourcePath); - - // Harvest the package contents and add it to the source files we need to compile. - string packageContentWxs = Path.Combine(msiSourcePath, "PackageContent.wxs"); - sourceFiles.Add(packageContentWxs); - - HarvestToolTask heat = new(BuildEngine, WixToolsetPath) - { - ComponentGroupName = GenerateMsiBase.PackageContentComponentGroupName, - DirectoryReference = "ManifestIdDir", - OutputFile = packageContentWxs, - Platform = platform, - SourceDirectory = packageContentsDataDirectory - }; - - if (!heat.Execute()) - { - throw new Exception($"Failed to harvest package contents."); - } - - // To support upgrades, the UpgradeCode must be stable withing a feature band. - // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform. - var upgradeCode = GenerateUpgradeCode(ManifestId, SdkFeatureBandVersion, platform); - var productCode = Guid.NewGuid(); - Log.LogMessage($"UC: {upgradeCode}, PC: {productCode}, {SdkFeatureBandVersion}, {SdkVersion}, {platform}"); - - string providerKeyName = $"{ManifestId},{SdkFeatureBandVersion},{platform}"; - - // Compile the MSI sources - string candleIntermediateOutputPath = Path.Combine(IntermediateBaseOutputPath, "wixobj", - $"{nupkg.Id}", $"{nupkg.Version}", platform); - - CompileToolTask candle = new(BuildEngine, WixToolsetPath) - { - // Candle expects the output path to end with a single '\' - OutputPath = candleIntermediateOutputPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, - SourceFiles = sourceFiles, - Arch = platform - }; - - // Configure preprocessor definitions. - string manufacturer = "Microsoft Corporation"; - - if (!string.IsNullOrWhiteSpace(nupkg.Authors) && (nupkg.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) - { - manufacturer = nupkg.Authors; - } - Log.LogMessage(MessageImportance.Low, $"Setting Manufacturer to '{manufacturer}'"); - - candle.PreprocessorDefinitions.Add($@"PackageId={nupkg.Id}"); - candle.PreprocessorDefinitions.Add($@"PackageVersion={nupkg.Version}"); - candle.PreprocessorDefinitions.Add($@"ProductVersion={MsiVersion}"); - candle.PreprocessorDefinitions.Add($@"ProductCode={productCode}"); - candle.PreprocessorDefinitions.Add($@"UpgradeCode={upgradeCode}"); - // Override the default provider key - candle.PreprocessorDefinitions.Add($@"DependencyProviderKeyName={providerKeyName}"); - candle.PreprocessorDefinitions.Add($@"ProductName={productName}"); - candle.PreprocessorDefinitions.Add($@"Platform={platform}"); - candle.PreprocessorDefinitions.Add($@"SourceDir={packageContentsDataDirectory}"); - candle.PreprocessorDefinitions.Add($@"Manufacturer={manufacturer}"); - candle.PreprocessorDefinitions.Add($@"EulaRtf={EulaRtfPath}"); - candle.PreprocessorDefinitions.Add($@"SdkFeatureBandVersion={SdkFeatureBandVersion}"); - - // The temporary installer in the SDK used lower invariants of the manifest ID. - // We have to do the same to ensure the keypath generation produces stable GUIDs so that - // the manifests/targets get the same component GUIDs. - candle.PreprocessorDefinitions.Add($@"ManifestId={ManifestId.ToLowerInvariant()}"); - - // Compiler extension to process dependency provider authoring for package reference counting. - candle.Extensions.Add("WixDependencyExtension"); - - if (!candle.Execute()) - { - throw new Exception($"Failed to compile MSI."); - } - - // Link the MSI. The generated filename contains a the semantic version (excluding build metadata) and platform. - // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add - // the platform again. - - string shortPackageName = Path.GetFileNameWithoutExtension(WorkloadManifestPackage); - - string outputFile = Path.Combine(OutputPath, shortPackageName + $"-{platform}.msi"); - - LinkToolTask light = new(BuildEngine, WixToolsetPath) - { - OutputFile = Path.Combine(OutputPath, outputFile), - SourceFiles = Directory.EnumerateFiles(candleIntermediateOutputPath, "*.wixobj"), - SuppressIces = this.SuppressIces - }; - - // Add WiX extensions - light.Extensions.Add("WixDependencyExtension"); - light.Extensions.Add("WixUIExtension"); - light.Extensions.Add("WixUtilExtension"); - - if (!light.Execute()) - { - throw new Exception($"Failed to link MSI."); - } - - // Generate metadata used for CLI based installations. - string msiPath = light.OutputFile; - MsiProperties msiProps = new MsiProperties - { - InstallSize = MsiUtils.GetInstallSize(msiPath), - Language = Convert.ToInt32(MsiUtils.GetProperty(msiPath, "ProductLanguage")), - Payload = Path.GetFileName(msiPath), - ProductCode = MsiUtils.GetProperty(msiPath, "ProductCode"), - ProductVersion = MsiUtils.GetProperty(msiPath, "ProductVersion"), - ProviderKeyName = $"{providerKeyName}", - UpgradeCode = MsiUtils.GetProperty(msiPath, "UpgradeCode"), - RelatedProducts = MsiUtils.GetRelatedProducts(msiPath) - }; - - string msiJsonPath = Path.Combine(Path.GetDirectoryName(msiPath), Path.GetFileNameWithoutExtension(msiPath) + ".json"); - File.WriteAllText(msiJsonPath, JsonSerializer.Serialize(msiProps)); - - TaskItem msi = new(light.OutputFile); - msi.SetMetadata(Metadata.Platform, platform); - msi.SetMetadata(Metadata.Version, nupkg.ProductVersion); - msi.SetMetadata(Metadata.JsonProperties, msiJsonPath); - msi.SetMetadata(Metadata.WixObj, candleIntermediateOutputPath); - - if (GenerateSwixAuthoring && IsSupportedByVisualStudio(platform)) - { - string swixPackageId = $"{nupkg.Id.ToString().Replace(ShortNames)}"; - - string swixProject = GenerateSwixPackageAuthoring(light.OutputFile, - swixPackageId, platform); - - if (!string.IsNullOrWhiteSpace(swixProject)) - { - msi.SetMetadata(Metadata.SwixProject, swixProject); - } - } - - // Generate a .csproj to build a NuGet payload package to carry the MSI and JSON manifest - msi.SetMetadata(Metadata.PackageProject, GeneratePackageProject(msi.ItemSpec, msiJsonPath, platform, nupkg)); - - msis.Add(msi); - } - - Msis = msis.ToArray(); - } - catch (Exception e) - { - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - private string GeneratePackageProject(string msiPath, string msiJsonPath, string platform, NugetPackage nupkg) - { - string msiPackageProject = Path.Combine(MsiPackageDirectory, platform, nupkg.Id, "msi.csproj"); - string msiPackageProjectDir = Path.GetDirectoryName(msiPackageProject); - - Log?.LogMessage($"Generating package project: '{msiPackageProject}'"); - - if (Directory.Exists(msiPackageProjectDir)) - { - Directory.Delete(msiPackageProjectDir, recursive: true); - } - - Directory.CreateDirectory(msiPackageProjectDir); - - string iconFileName = "Icon.png"; - string licenseFileName = "LICENSE.TXT"; - EmbeddedTemplates.Extract(iconFileName, msiPackageProjectDir); - EmbeddedTemplates.Extract(licenseFileName, msiPackageProjectDir); - - XmlWriterSettings settings = new XmlWriterSettings - { - Indent = true, - IndentChars = " ", - }; - - XmlWriter writer = XmlWriter.Create(msiPackageProject, settings); - - writer.WriteStartElement("Project"); - writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); - - writer.WriteStartElement("PropertyGroup"); - writer.WriteElementString("TargetFramework", "net5.0"); - writer.WriteElementString("GeneratePackageOnBuild", "true"); - writer.WriteElementString("IncludeBuildOutput", "false"); - writer.WriteElementString("IsPackable", "true"); - writer.WriteElementString("PackageType", "DotnetPlatform"); - writer.WriteElementString("SuppressDependenciesWhenPacking", "true"); - writer.WriteElementString("NoWarn", "$(NoWarn);NU5128"); - writer.WriteElementString("PackageId", $"{nupkg.Id}.Msi.{platform}"); - writer.WriteElementString("PackageVersion", $"{nupkg.Version}"); - writer.WriteElementString("Description", nupkg.Description); - - if (!string.IsNullOrWhiteSpace(nupkg.Authors)) - { - writer.WriteElementString("Authors", nupkg.Authors); - } - - if (!string.IsNullOrWhiteSpace(nupkg.Copyright)) - { - writer.WriteElementString("Copyright", nupkg.Copyright); - } - - if (!string.IsNullOrWhiteSpace(nupkg.ProjectUrl)) - { - writer.WriteElementString("PackageProjectUrl", nupkg.ProjectUrl); - } - - writer.WriteElementString("PackageLicenseExpression", "MIT"); - writer.WriteEndElement(); - - writer.WriteStartElement("ItemGroup"); - WriteItem(writer, "None", msiPath, @"\data"); - WriteItem(writer, "None", msiJsonPath, @"\data\msi.json"); - WriteItem(writer, "None", licenseFileName, @"\"); - writer.WriteEndElement(); // ItemGroup - - writer.WriteRaw(@" - - - Icon.png - - - - - -"); - - writer.WriteEndElement(); // Project - writer.Flush(); - writer.Close(); - - return msiPackageProject; - } - - private void WriteItem(XmlWriter writer, string itemName, string include, string packagePath) - { - writer.WriteStartElement(itemName); - writer.WriteAttributeString("Include", include); - writer.WriteAttributeString("Pack", "true"); - writer.WriteAttributeString("PackagePath", packagePath); - writer.WriteEndElement(); - } - - internal string GenerateSwixPackageAuthoring(string msiPath, string packageId, string platform) - { - GenerateVisualStudioMsiPackageProject swixTask = new() - { - Chip = platform, - IntermediateBaseOutputPath = this.IntermediateBaseOutputPath, - PackageName = packageId, - MsiPath = msiPath, - Version = !string.IsNullOrEmpty(MsiVersion) ? new Version(MsiVersion) : null, - BuildEngine = this.BuildEngine, - }; - - string vsPayloadRelativePath = $"{swixTask.PackageName},version={swixTask.Version.ToString(3)},chip={swixTask.Chip},productarch={swixTask.ProductArch}\\{Path.GetFileName(msiPath)}"; - CheckRelativePayloadPath(vsPayloadRelativePath); - - if (!swixTask.Execute()) - { - Log.LogError($"Failed to generate SWIX authoring for '{msiPath}'"); - } - - return swixTask.SwixProject; - } - - internal static string GetManifestIdFromPackageId(string packageId) - { - return !string.IsNullOrWhiteSpace(packageId) && packageId.IndexOf(ManifestSeparator) > -1 ? - packageId.Substring(0, packageId.IndexOf(ManifestSeparator)) : - null; - } - - internal static string GetSdkVersionFromPackageId(string packageId) - { - return !string.IsNullOrWhiteSpace(packageId) && packageId.IndexOf(ManifestSeparator) > -1 ? - packageId.Substring(packageId.IndexOf(ManifestSeparator) + ManifestSeparator.Length) : - null; - } - - internal static ReleaseVersion GetSdkFeatureBandVersion(string version) - { - ReleaseVersion sdkVersion = new(version); - - if (string.IsNullOrEmpty(sdkVersion.Prerelease) || sdkVersion.Prerelease.Split('.').Any(s => string.Equals("ci", s) || string.Equals("dev", s))) - { - return new ReleaseVersion(sdkVersion.Major, sdkVersion.Minor, sdkVersion.SdkFeatureBand); - } - - string[] preleaseParts = sdkVersion.Prerelease.Split('.'); - - string prerelease = (preleaseParts.Length > 1) ? - $"{preleaseParts[0]}.{preleaseParts[1]}" : - preleaseParts[0]; - - return new ReleaseVersion(sdkVersion.Major, sdkVersion.Minor, sdkVersion.SdkFeatureBand, prerelease); - } - - internal static Guid GenerateUpgradeCode(string manifestId, ReleaseVersion sdkFeatureBandVersion, string platform) - { - return Utils.CreateUuid(GenerateMsiBase.UpgradeCodeNamespaceUuid, $"{manifestId};{sdkFeatureBandVersion};{platform}"); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsi.wix.cs deleted file mode 100644 index 0c34271f6fe..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsi.wix.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.NET.Sdk.WorkloadManifestReader; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task to generate a workload pack installer (MSI) from a NuGet package. - /// - public class GenerateMsi : GenerateMsiBase - { - /// - /// The kind of package, e.g. framework, sdk, template, etc. - /// - [Required] - public string Kind - { - get; - set; - } - - /// - /// The target platforms to use for generating MSIs. - /// - [Required] - public ITaskItem[] Platforms - { - get; - set; - } - - /// - /// The path of the NuGet package to convert into an MSI. - /// - [Required] - public string SourcePackage - { - get; - set; - } - - public override bool Execute() - { - try - { - if (!Enum.TryParse(Kind, true, out WorkloadPackKind kind)) - { - Log.LogError($"Invalid package kind ({Kind})."); - return false; - } - - string[] platforms = Platforms.Select(p => p.ItemSpec).ToArray(); - IEnumerable unsupportedPlatforms = platforms.Except(SupportedPlatforms); - - if (unsupportedPlatforms.Count() > 0) - { - Log.LogError($"Unsupported platforms detected: {String.Join(",", unsupportedPlatforms)}."); - return false; - } - - // For a single MSI we always generate all platforms and simply use the ID of the source package for - // the SWIX projects. - List msis = new(); - msis.AddRange(Generate(SourcePackage, null, OutputPath, kind, platforms)); - Msis = msis.ToArray(); - } - catch (Exception e) - { - Log.LogMessage(MessageImportance.Low, e.StackTrace); - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsiBase.wix.cs deleted file mode 100644 index 1bbf2c5b318..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateMsiBase.wix.cs +++ /dev/null @@ -1,478 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Xml; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task to generate a workload pack installer (MSI). - /// - public abstract class GenerateMsiBase : GenerateTaskBase - { - /// - /// The name of the ComponentGroup generated by Heat. - /// - internal const string PackageContentComponentGroupName = "CG_PackageContent"; - - /// - /// The DirectoryReference to use as the parent for harvested directories. - /// - internal const string PackageContentDirectoryReference = "VersionDir"; - - /// - /// The UUID namespace to use for generating a product code. - /// - internal static readonly Guid ProductCodeNamespaceUuid = Guid.Parse("3B04DD8B-41C4-4DA3-9E49-4B69F11533A7"); - - /// - /// Static RTF text for inserting a EULA into the MSI. The license URL of the NuGet package will be embedded - /// as plain text since the text control used to render the MSI UI does not render hyperlinks even though RTF supports it. - /// - internal static readonly string Eula = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} -{\colortbl ;\red0\green0\blue255;} -{\*\generator Riched20 10.0.19041}\viewkind4\uc1 -\pard\sa200\sl276\slmult1\f0\fs22\lang9 This software is licensed separately as set out in its accompanying license. By continuing, you also agree to that license (__LICENSE_URL__).\par -\par -}"; - - /// - /// An item group containing information to shorten the names of packages. - /// - public ITaskItem[] ShortNames - { - get; - set; - } - - /// - /// The set of supported target platforms for MSIs. - /// - internal static readonly string[] SupportedPlatforms = new string[] { "x86", "x64", "arm64" }; - - /// - /// The UUID namesapce to use for generating an upgrade code. - /// - internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C"); - - /// - /// Wildcard patterns of files that should be removed prior to harvesting the package contents. - /// - public ITaskItem[] ExcludeFiles - { - get; - set; - } = Array.Empty(); - - /// - /// Gets or sets whether a corresponding SWIX project should be generated for the MSI. - /// - public bool GenerateSwixAuthoring - { - get; - set; - } - - /// - /// The set of MSIs that were produced. - /// - [Output] - public ITaskItem[] Msis - { - get; - protected set; - } - - /// - /// The path where the generated MSIs will be placed. - /// - [Required] - public string OutputPath - { - get; - set; - } - - /// - /// Semicolon sepearate list of ICEs to suppress. - /// - public string SuppressIces - { - get; - set; - } - - /// - /// Used to synchronize access to the NuGet being extracted. - /// - private readonly object extractNuGetLock = new object(); - - /// - /// Generate a set of MSIs for the specified platforms using the specified NuGet package. - /// - /// The NuGet package to convert into an MSI. - /// The output path of the generated MSI. - /// - protected IEnumerable Generate(string sourcePackage, string swixPackageId, string outputPath, WorkloadPackKind kind, params string[] platforms) - { - NugetPackage nupkg = null; - lock (extractNuGetLock) - { - nupkg = new(sourcePackage, Log); - } - List msis = new(); - - // MSI ProductName defaults to the package title and fallback to the package ID with a warning. - string productName = nupkg.Title; - - if (string.IsNullOrWhiteSpace(nupkg.Title)) - { - Log?.LogMessage(MessageImportance.High, $"'{sourcePackage}' should have a non-empty title. The MSI ProductName will be set to the package ID instead."); - productName = nupkg.Id; - } - - // Extract once, but harvest multiple times because some generated attributes are platform dependent. - string packageContentsDirectory = Path.Combine(PackageDirectory, $"{nupkg.Identity}"); - IEnumerable exclusions = GetExlusionPatterns(); - string installDir = GetInstallDir(kind); - string packKind = kind.ToString().ToLowerInvariant(); - - if ((kind != WorkloadPackKind.Library) && (kind != WorkloadPackKind.Template)) - { - Log.LogMessage(MessageImportance.Low, $"Extracting '{sourcePackage}' to '{packageContentsDirectory}'"); - lock (extractNuGetLock) - { - nupkg.Extract(packageContentsDirectory, exclusions); - } - } - else - { - // Library and template packs are not extracted. We want to harvest the nupkg itself, - // instead of the contents. The package is still copied to a separate folder for harvesting - // to avoid accidentally pulling in additional files and directories. - Log.LogMessage(MessageImportance.Low, $"Copying '{sourcePackage}' to '{packageContentsDirectory}'"); - - if (Directory.Exists(packageContentsDirectory)) - { - Directory.Delete(packageContentsDirectory, recursive: true); - } - Directory.CreateDirectory(packageContentsDirectory); - - File.Copy(sourcePackage, Path.Combine(packageContentsDirectory, Path.GetFileName(sourcePackage))); - } - - System.Threading.Tasks.Parallel.ForEach(platforms, platform => - { - // Extract the MSI template and add it to the list of source files. - List sourceFiles = new(); - string msiSourcePath = Path.Combine(MsiDirectory, $"{nupkg.Id}", $"{nupkg.Version}", platform); - sourceFiles.Add(EmbeddedTemplates.Extract("DependencyProvider.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("Directories.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("dotnethome_x64.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("Product.wxs", msiSourcePath)); - sourceFiles.Add(EmbeddedTemplates.Extract("Registry.wxs", msiSourcePath)); - - string EulaRtfPath = Path.Combine(msiSourcePath, "eula.rtf"); - File.WriteAllText(EulaRtfPath, Eula.Replace("__LICENSE_URL__", nupkg.LicenseUrl)); - EmbeddedTemplates.Extract("Variables.wxi", msiSourcePath); - - // Harvest the package contents and add it to the source files we need to compile. - string packageContentWxs = Path.Combine(msiSourcePath, "PackageContent.wxs"); - sourceFiles.Add(packageContentWxs); - - string directoryReference = (kind == WorkloadPackKind.Library) || (kind == WorkloadPackKind.Template) - ? "InstallDir" - : PackageContentDirectoryReference; - - HarvestToolTask heat = new(BuildEngine, WixToolsetPath) - { - ComponentGroupName = PackageContentComponentGroupName, - DirectoryReference = directoryReference, - OutputFile = packageContentWxs, - Platform = platform, - SourceDirectory = packageContentsDirectory - }; - - lock (extractNuGetLock) - { - if (!heat.Execute()) - { - throw new Exception($"Failed to harvest package contents."); - } - } - - // Compile the MSI sources - string candleIntermediateOutputPath = Path.Combine(IntermediateBaseOutputPath, "wixobj", - $"{nupkg.Id}", $"{nupkg.Version}", platform); - - CompileToolTask candle = new(BuildEngine, WixToolsetPath) - { - // Candle expects the output path to end with a single '\' - OutputPath = candleIntermediateOutputPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, - SourceFiles = sourceFiles, - Arch = platform - }; - - // Configure preprocessor definitions. - string manufacturer = "Microsoft Corporation"; - - if (!string.IsNullOrWhiteSpace(nupkg.Authors) && (nupkg.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) - { - manufacturer = nupkg.Authors; - } - Log.LogMessage(MessageImportance.Low, $"Setting Manufacturer to '{manufacturer}'"); - - candle.PreprocessorDefinitions.AddRange(GetPreprocessorDefinitions(nupkg, platform)); - candle.PreprocessorDefinitions.Add($@"InstallDir={installDir}"); - candle.PreprocessorDefinitions.Add($@"ProductName={productName}"); - candle.PreprocessorDefinitions.Add($@"Platform={platform}"); - candle.PreprocessorDefinitions.Add($@"SourceDir={packageContentsDirectory}"); - candle.PreprocessorDefinitions.Add($@"Manufacturer={manufacturer}"); - candle.PreprocessorDefinitions.Add($@"EulaRtf={EulaRtfPath}"); - candle.PreprocessorDefinitions.Add($@"PackKind={packKind}"); - - // Compiler extension to process dependency provider authoring for package reference counting. - candle.Extensions.Add("WixDependencyExtension"); - - if (!candle.Execute()) - { - throw new Exception($"Failed to compile MSI."); - } - - // Link the MSI. The generated filename contains a the semantic version (excluding build metadata) and platform. - // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add - // the platform again. - - string shortPackageName = Path.GetFileNameWithoutExtension(sourcePackage).Replace(ShortNames); - - string outputFile = sourcePackage.Contains(platform) ? - Path.Combine(OutputPath, shortPackageName + ".msi") : - Path.Combine(OutputPath, shortPackageName + $"-{platform}.msi"); - - LinkToolTask light = new(BuildEngine, WixToolsetPath) - { - OutputFile = Path.Combine(OutputPath, outputFile), - SourceFiles = Directory.EnumerateFiles(candleIntermediateOutputPath, "*.wixobj"), - SuppressIces = this.SuppressIces - }; - - // Add WiX extensions - light.Extensions.Add("WixDependencyExtension"); - light.Extensions.Add("WixUIExtension"); - light.Extensions.Add("WixUtilExtension"); - - if (!light.Execute()) - { - throw new Exception($"Failed to link MSI."); - } - - // Generate metadata used for CLI based installations. - string msiPath = light.OutputFile; - MsiProperties msiProps = new MsiProperties - { - InstallSize = MsiUtils.GetInstallSize(msiPath), - Language = Convert.ToInt32(MsiUtils.GetProperty(msiPath, "ProductLanguage")), - Payload = Path.GetFileName(msiPath), - ProductCode = MsiUtils.GetProperty(msiPath, "ProductCode"), - ProductVersion = MsiUtils.GetProperty(msiPath, "ProductVersion"), - ProviderKeyName = $"{nupkg.Id},{nupkg.Version},{platform}", - UpgradeCode = MsiUtils.GetProperty(msiPath, "UpgradeCode"), - RelatedProducts = MsiUtils.GetRelatedProducts(msiPath) - }; - - string msiJsonPath = Path.Combine(Path.GetDirectoryName(msiPath), Path.GetFileNameWithoutExtension(msiPath) + ".json"); - File.WriteAllText(msiJsonPath, JsonSerializer.Serialize(msiProps)); - - TaskItem msi = new(light.OutputFile); - msi.SetMetadata(Metadata.Platform, platform); - msi.SetMetadata(Metadata.Version, nupkg.ProductVersion); - msi.SetMetadata(Metadata.JsonProperties, msiJsonPath); - msi.SetMetadata(Metadata.WixObj, candleIntermediateOutputPath); - - if (GenerateSwixAuthoring && IsSupportedByVisualStudio(platform)) - { - string swixProject = GenerateSwixPackageAuthoring(light.OutputFile, - !string.IsNullOrWhiteSpace(swixPackageId) ? swixPackageId : - $"{nupkg.Id.Replace(ShortNames)}.{nupkg.Version}", platform); - - if (!string.IsNullOrWhiteSpace(swixProject)) - { - msi.SetMetadata(Metadata.SwixProject, swixProject); - } - } - - // Generate a .csproj to build a NuGet payload package to carry the MSI and JSON manifest - msi.SetMetadata(Metadata.PackageProject, GeneratePackageProject(msi.ItemSpec, msiJsonPath, platform, nupkg)); - - msis.Add(msi); - }); - - return msis; - } - - private string GeneratePackageProject(string msiPath, string msiJsonPath, string platform, NugetPackage nupkg) - { - string msiPackageProject = Path.Combine(MsiPackageDirectory, platform, nupkg.Id, "msi.csproj"); - string msiPackageProjectDir = Path.GetDirectoryName(msiPackageProject); - - Log?.LogMessage($"Generating package project: '{msiPackageProject}'"); - - if (Directory.Exists(msiPackageProjectDir)) - { - Directory.Delete(msiPackageProjectDir, recursive: true); - } - - Directory.CreateDirectory(msiPackageProjectDir); - - string iconFileName = "Icon.png"; - string licenseFileName = "LICENSE.TXT"; - EmbeddedTemplates.Extract(iconFileName, msiPackageProjectDir); - EmbeddedTemplates.Extract(licenseFileName, msiPackageProjectDir); - - XmlWriterSettings settings = new XmlWriterSettings - { - Indent = true, - IndentChars = " ", - }; - - XmlWriter writer = XmlWriter.Create(msiPackageProject, settings); - - writer.WriteStartElement("Project"); - writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); - - writer.WriteStartElement("PropertyGroup"); - writer.WriteElementString("TargetFramework", "net5.0"); - writer.WriteElementString("GeneratePackageOnBuild", "true"); - writer.WriteElementString("IncludeBuildOutput", "false"); - writer.WriteElementString("IsPackable", "true"); - writer.WriteElementString("PackageType", "DotnetPlatform"); - writer.WriteElementString("SuppressDependenciesWhenPacking", "true"); - writer.WriteElementString("NoWarn", "$(NoWarn);NU5128"); - writer.WriteElementString("PackageId", $"{nupkg.Id}.Msi.{platform}"); - writer.WriteElementString("PackageVersion", $"{nupkg.Version}"); - writer.WriteElementString("Description", nupkg.Description); - - if (!string.IsNullOrWhiteSpace(nupkg.Authors)) - { - writer.WriteElementString("Authors", nupkg.Authors); - } - - if (!string.IsNullOrWhiteSpace(nupkg.Copyright)) - { - writer.WriteElementString("Copyright", nupkg.Copyright); - } - - if (!string.IsNullOrWhiteSpace(nupkg.ProjectUrl)) - { - writer.WriteElementString("PackageProjectUrl", nupkg.ProjectUrl); - } - - writer.WriteElementString("PackageLicenseExpression", "MIT"); - writer.WriteEndElement(); - - writer.WriteStartElement("ItemGroup"); - WriteItem(writer, "None", msiPath, @"\data"); - WriteItem(writer, "None", msiJsonPath, @"\data\msi.json"); - WriteItem(writer, "None", licenseFileName, @"\"); - writer.WriteEndElement(); // ItemGroup - - writer.WriteRaw(@" - - - Icon.png - - - - - -"); - - writer.WriteEndElement(); // Project - writer.Flush(); - writer.Close(); - - return msiPackageProject; - } - - private void WriteItem(XmlWriter writer, string itemName, string include, string packagePath) - { - writer.WriteStartElement(itemName); - writer.WriteAttributeString("Include", include); - writer.WriteAttributeString("Pack", "true"); - writer.WriteAttributeString("PackagePath", packagePath); - writer.WriteEndElement(); - } - - private string GenerateSwixPackageAuthoring(string msiPath, string packageId, string platform) - { - GenerateVisualStudioMsiPackageProject swixTask = new() - { - Chip = platform, - IntermediateBaseOutputPath = this.IntermediateBaseOutputPath, - PackageName = packageId, - MsiPath = msiPath, - BuildEngine = this.BuildEngine, - }; - - if (!swixTask.Execute()) - { - Log.LogError($"Failed to generate SWIX authoring for '{msiPath}'"); - } - - return swixTask.SwixProject; - } - - private IEnumerable GetExlusionPatterns() - { - IEnumerable patterns = ExcludeFiles.Select( - e => Utils.ConvertToRegexPattern(e.ItemSpec)) ?? Enumerable.Empty(); - - foreach (string pattern in patterns) - { - Log.LogMessage(MessageImportance.Low, $"Adding exclusion pattern: {pattern}"); - } - - return patterns; - } - - /// - /// Generate a set of preprocessor variable definitions using the metadata. - /// - /// An enumerable containing package metadata converted to WiX preprocessor definitions. - private IEnumerable GetPreprocessorDefinitions(NugetPackage package, string platform) - { - yield return $@"PackageId={package.Id}"; - yield return $@"PackageVersion={package.Version}"; - yield return $@"ProductVersion={package.ProductVersion}"; - yield return $@"ProductCode={Utils.CreateUuid(ProductCodeNamespaceUuid, package.Identity.ToString() + $"{platform}"):B}"; - yield return $@"UpgradeCode={Utils.CreateUuid(UpgradeCodeNamespaceUuid, package.Identity.ToString() + $"{platform}"):B}"; - } - - /// - /// Get the installation directory based on the kind of workload pack. - /// - /// The workload pack kind. - /// The name of the root installation directory. - internal static string GetInstallDir(WorkloadPackKind kind) - { - return kind switch - { - WorkloadPackKind.Framework or WorkloadPackKind.Sdk => "packs", - WorkloadPackKind.Library => "library-packs", - WorkloadPackKind.Template => "template-packs", - WorkloadPackKind.Tool => "tool-packs", - _ => throw new ArgumentException($"Unknown package kind: {kind}"), - }; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateTaskBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateTaskBase.cs deleted file mode 100644 index 2f64819c60a..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateTaskBase.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public abstract class GenerateTaskBase : Microsoft.Build.Utilities.Task - { - public const int MaxPayloadRelativePath = 182; - - public static readonly string[] SupportedVisualStudioPlatforms = { "x86", "x64" }; - - /// - /// The root intermediate output directory. - /// - [Required] - public string IntermediateBaseOutputPath - { - get; - set; - } - - /// - /// Root directory for generated source files. - /// - public string SourceDirectory => Path.Combine(IntermediateBaseOutputPath, "src"); - - /// - /// Root directory for extracting package content. - /// - public string PackageDirectory => Path.Combine(IntermediateBaseOutputPath, "pkg"); - - /// - /// Root directory for generated SWIX projects. - /// - public string SwixDirectory => Path.Combine(SourceDirectory, "swix"); - - /// - /// Root directory for generated MSI sources. - /// - public string MsiDirectory => Path.Combine(SourceDirectory, "msi"); - - /// - /// Root directory for .csproj sources to build NuGet packages. - /// - public string MsiPackageDirectory => Path.Combine(SourceDirectory, "msiPackage"); - - /// - /// The directory containing the WiX toolset binaries. - /// - [Required] - public string WixToolsetPath - { - get; - set; - } - - /// - /// Determines if the specified platfor is support by Visual Studio. - /// - /// The platform to check - /// if the platform is supported by Visual Studio. - protected bool IsSupportedByVisualStudio(string platform) - { - return SupportedVisualStudioPlatforms.Contains(platform); - } - - protected void CheckRelativePayloadPath(string relativePath) - { - if (relativePath.Length > MaxPayloadRelativePath) - { - // We'll let the task's execute method take care of logging this and terminating. - throw new Exception($"Payload relative path exceeds max length ({MaxPayloadRelativePath}): {relativePath}"); - } - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioManifest.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioManifest.cs deleted file mode 100644 index 825d9e887a4..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioManifest.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task for generating a Visual Studio manifest project (.vsmanproj). The generated project can be used - /// to create a manifest (.vsman) by merging JSON manifest files produced from one or more SWIX project. - /// - public class GenerateVisualStudioManifest : Microsoft.Build.Utilities.Task - { - /// - /// The base path where the project source will be generated. - /// - [Required] - public string IntermediateOutputBase - { - get; - set; - } - - internal string IntermediateOutputPath => Path.Combine(IntermediateOutputBase, ManifestName); - - [Required] - public string ManifestName - { - get; - set; - } - - [Required] - public string ProductFamily - { - get; - set; - } - - [Required] - public string ProductVersion - { - get; - set; - } - - [Required] - public string ProductName - { - get; - set; - } - - [Output] - public string GeneratedManifestProject - { - get; - set; - } - - public override bool Execute() - { - try - { - GeneratedManifestProject = EmbeddedTemplates.Extract("manifest.vsmanproj", IntermediateOutputPath, ManifestName + ".vsmanproj"); - Utils.StringReplace(GeneratedManifestProject, GetReplacementTokens(), Encoding.UTF8); - } - catch (Exception e) - { - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - private Dictionary GetReplacementTokens() - { - return new Dictionary() - { - {"__PRODUCT_NAME__", ProductName }, - {"__PRODUCT_FAMILY__", ProductFamily }, - {"__PRODUCT_VERSION__", ProductVersion }, - }; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioMsiPackageProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioMsiPackageProject.wix.cs deleted file mode 100644 index 3b2573fa438..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioMsiPackageProject.wix.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Microsoft.Build.Framework; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task for generating a Visual Studio MSI package project (.swixproj). - /// - public class GenerateVisualStudioMsiPackageProject : GenerateTaskBase - { - /// - /// The OS architecture targeted by the MSI. - /// - [Required] - public string Chip - { - get; - set; - } - - /// - /// The path of the MSI file. - /// - public string MsiPath - { - get; - set; - } - - /// - /// The name of the Visual Studio package (ID), e.g. "Microsoft.VisualStudio.X.Y.Z", that will wrap the MSI. - /// - [Required] - public string PackageName - { - get; - set; - } - - /// - /// The version of the MSI payload package in the Visual Studio manifest. - /// - public Version Version - { - get; - set; - } - - /// - /// The path of the generated .swixproj file. - /// - [Output] - public string SwixProject - { - get; - set; - } - - /// - /// The size of the MSI in bytes. - /// - internal long PayloadSize - { - get; - set; - } - - /// - /// The size of the installation in bytes. The size is an estimate based on the data in the File table, multiplied - /// by a factor to account for registry entries in the component database, a.k.a, Darwin descriptors. - /// - internal long InstallSize - { - get; - set; - } - - /// - /// The product architecture to which the package applies, the default is neutral. - /// - internal string ProductArch - { - get; - set; - } = "neutral"; - - public override bool Execute() - { - try - { - Log.LogMessage($"Generating SWIX package authoring for '{MsiPath}'"); - - if (Version == null) - { - // Use the version of the MSI if none was specified - Version = new Version(MsiUtils.GetProperty(MsiPath, "ProductVersion")); - - Log.LogMessage($"Using MSI version for package version: {Version}"); - } - - // Try to catch VS manifest validation errors before we get to VS. - string vsPayloadRelativePath = $"{PackageName},version={Version.ToString(3)},chip={Chip},productarch={ProductArch}\\{Path.GetFileName(MsiPath)}"; - CheckRelativePayloadPath(vsPayloadRelativePath); - - string swixSourceDirectory = Path.Combine(SwixDirectory, PackageName, Chip); - string msiSwr = EmbeddedTemplates.Extract("msi.swr", swixSourceDirectory); - string fullProjectName = $"{PackageName}.{Version.ToString(3)}.{Chip}"; - string msiSwixProj = EmbeddedTemplates.Extract("msi.swixproj", swixSourceDirectory, $"{Utils.GetHash(fullProjectName, "MD5")}.swixproj"); - - FileInfo msiInfo = new(MsiPath); - PayloadSize = msiInfo.Length; - InstallSize = MsiUtils.GetInstallSize(MsiPath); - Log.LogMessage($"MSI payload size: {PayloadSize}, install size (estimated): {InstallSize} "); - - Utils.StringReplace(msiSwr, GetReplacementTokens(), Encoding.UTF8); - Utils.StringReplace(msiSwixProj, GetReplacementTokens(), Encoding.UTF8); - - SwixProject = msiSwixProj; - } - catch (Exception e) - { - Log.LogMessage(e.StackTrace); - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - private Dictionary GetReplacementTokens() - { - return new Dictionary() - { - {"__VS_PACKAGE_NAME__", PackageName }, - {"__VS_PACKAGE_VERSION__", Version.ToString() }, - {"__VS_PACKAGE_CHIP__", Chip }, - {"__VS_PACKAGE_INSTALL_SIZE_SYSTEM_DRIVE__", $"{InstallSize}"}, - {"__VS_PACKAGE_PRODUCT_ARCH__", $"{ProductArch}" }, - {"__VS_PAYLOAD_SOURCE__", MsiPath }, - {"__VS_PAYLOAD_SIZE__", $"{PayloadSize}" }, - }; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioWorkload.wix.cs deleted file mode 100644 index 356daf35983..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateVisualStudioWorkload.wix.cs +++ /dev/null @@ -1,312 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task for generating Visual Studio component projects representing - /// the workload definitions. - /// - public class GenerateVisualStudioWorkload : GenerateTaskBase - { - /// - /// An item group used to provide a customized title, description, and category for a specific workload ID in Visual Studio. - /// Workloads only define a description. Visual Studio defines a separate title (checkbox text) and description (checkbox tooltip). - /// - public ITaskItem[] ComponentResources - { - get; - set; - } - - /// - /// The version of the component in the Visual Studio manifest. If no version is specified, - /// the manifest version is used. - /// - public ITaskItem[] ComponentVersions - { - get; - set; - } - - /// - /// Gets or sets whether MSIs for workload packs will be generated. When set to , only - /// Visual Studio component authoring files are generated. - /// - public bool GenerateMsis - { - get; - set; - } = true; - - /// - /// Set of missing workload pack packages. - /// - [Output] - public ITaskItem[] MissingPacks - { - get; - set; - } = Array.Empty(); - - public string OutputPath - { - get; - set; - } - - /// - /// The path where the workload-pack packages referenced by the workload manifests are located. - /// - public string PackagesPath - { - get; - set; - } - - /// - /// An item group containing information to shorten the names of packages. - /// - public ITaskItem[] ShortNames - { - get; - set; - } - - /// - /// The workload manifest files to use for generating the Visual Studio components. - /// - public ITaskItem[] WorkloadManifests - { - get; - set; - } - - /// - /// A set of packages containing workload manifests. - /// - public ITaskItem[] WorkloadPackages - { - get; - set; - } - - /// - /// Semicolon sepearate list of ICEs to suppress. - /// - public string SuppressIces - { - get; - set; - } - - /// - /// Generate msis in parallel. - /// - public bool RunInParallel - { - get; - set; - } = true; - - /// - /// The paths of the generated .swixproj files. - /// - [Output] - public ITaskItem[] SwixProjects - { - get; - set; - } - - [Output] - public ITaskItem[] Msis - { - get; - set; - } - - public override bool Execute() - { - try - { - if (WorkloadManifests != null) - { - SwixProjects = GenerateSwixProjects(WorkloadManifests); - } - else if (WorkloadPackages != null) - { - SwixProjects = GenerateSwixProjects(GetManifestsFromManifestPackages(WorkloadPackages)); - } - else - { - Log.LogError($"Either {nameof(WorkloadPackages)} or {nameof(WorkloadManifests)} item must be non-empty"); - return false; - } - } - catch (Exception e) - { - Log.LogMessage(MessageImportance.Low, e.StackTrace); - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - ITaskItem[] GenerateSwixProjects(ITaskItem[] workloadManifests) - { - List swixProjects = new(); - - // Generate the MSIs first to see if we have missing packs so we can remove - // those dependencies from the components. - if (GenerateMsis) - { - Log?.LogMessage(MessageImportance.Low, "Generating MSIs..."); - // Generate MSIs for workload packs and add their .swixproj files - swixProjects.AddRange(GenerateMsisFromManifests(workloadManifests)); - } - - foreach (ITaskItem workloadManifest in workloadManifests) - { - swixProjects.AddRange(ProcessWorkloadManifestFile(workloadManifest.GetMetadata("FullPath"))); - } - - return swixProjects.ToArray(); - } - - internal IEnumerable GenerateMsisFromManifests(ITaskItem[] workloadManifests) - { - GenerateWorkloadMsis msiTask = new() - { - BuildEngine = this.BuildEngine, - GenerateSwixAuthoring = true, - IntermediateBaseOutputPath = this.IntermediateBaseOutputPath, - OutputPath = this.OutputPath, - PackagesPath = this.PackagesPath, - RunInParallel = this.RunInParallel, - ShortNames = this.ShortNames, - SuppressIces = this.SuppressIces, - WixToolsetPath = this.WixToolsetPath, - WorkloadManifests = workloadManifests - }; - - if (!msiTask.Execute()) - { - Log?.LogError($"Failed to generate MSIs for workload packs."); - return Enumerable.Empty(); - } - else - { - if (msiTask.MissingPacks != null) - { - MissingPacks = msiTask.MissingPacks; - - foreach (ITaskItem item in MissingPacks) - { - Log?.LogMessage(MessageImportance.High, $"Unable to locate '{item.GetMetadata(Metadata.SourcePackage)}'. Short name: {item.GetMetadata(Metadata.ShortName)}, Platform: {item.GetMetadata(Metadata.Platform)}, Workload Pack: ({item.ItemSpec})."); - } - } - - Msis = msiTask.Msis; - - // The Msis output parameter also contains the .swixproj files, but for VS, we want all the project files for - // packages and components. - return msiTask.Msis.Where(m => !string.IsNullOrWhiteSpace(m.GetMetadata(Metadata.SwixProject))). - Select(m => new TaskItem(m.GetMetadata(Metadata.SwixProject))); - } - } - - internal IEnumerable ProcessWorkloadManifestFile(string workloadManifestJsonPath) - { - WorkloadManifest manifest = WorkloadManifestReader.ReadWorkloadManifest( - Path.GetFileNameWithoutExtension(workloadManifestJsonPath), File.OpenRead(workloadManifestJsonPath), workloadManifestJsonPath); - - List swixProjects = new(); - - foreach (WorkloadDefinition workloadDefinition in manifest.Workloads.Values) - { - if ((workloadDefinition.Platforms?.Count > 0) && (!workloadDefinition.Platforms.Any(p => p.StartsWith("win")))) - { - Log?.LogMessage(MessageImportance.High, $"{workloadDefinition.Id} platforms does not support Windows and will be skipped ({string.Join(", ", workloadDefinition.Platforms)})."); - continue; - } - - // Each workload maps to a Visual Studio component. - VisualStudioComponent component = VisualStudioComponent.Create(Log, manifest, workloadDefinition, - ComponentVersions, ShortNames, ComponentResources, MissingPacks); - - // If there are no dependencies, regardless of whether we are generating MSIs, we'll report an - // error as we'd produce invalid SWIX. - if (!component.HasDependencies) - { - Log?.LogError($"Visual Studio components '{component.Name}' must have at least one dependency."); - } - - string vsPayloadRelativePath = $"{component.Name},version={component.Version}\\_package.json"; - CheckRelativePayloadPath(vsPayloadRelativePath); - - swixProjects.Add(component.Generate(Path.Combine(SourceDirectory, $"{workloadDefinition.Id}.{manifest.Version}.0"))); - } - - return swixProjects; - } - - /// - /// Extracts the workload manifest from the manifest package and generate a SWIX project for a Visual Studio component - /// matching the manifests dependencies. - /// - /// The path of the workload package containing the manifest. - /// A set of items containing the generated SWIX projects. - internal IEnumerable ProcessWorkloadManifestPackage(string workloadManifestPackage) - { - NugetPackage workloadPackage = new(workloadManifestPackage, Log); - string packageContentPath = Path.Combine(PackageDirectory, $"{workloadPackage.Identity}"); - workloadPackage.Extract(packageContentPath, Enumerable.Empty()); - - return ProcessWorkloadManifestFile(GetWorkloadManifestJsonPath(packageContentPath)); - } - - internal ITaskItem[] GetManifestsFromManifestPackages(ITaskItem[] workloadPackages) - { - List manifests = new(); - - foreach (ITaskItem item in workloadPackages) - { - NugetPackage workloadPackage = new(item.GetMetadata("FullPath"), Log); - string packageContentPath = Path.Combine(PackageDirectory, $"{workloadPackage.Identity}"); - workloadPackage.Extract(packageContentPath, Enumerable.Empty()); - string workloadManifestJsonPath = GetWorkloadManifestJsonPath(packageContentPath); - Log?.LogMessage(MessageImportance.Low, $"Adding manifest: {workloadManifestJsonPath}"); - - manifests.Add(new TaskItem(workloadManifestJsonPath)); - } - - return manifests.ToArray(); - } - - internal string GetWorkloadManifestJsonPath(string packageContentPath) - { - string dataDirectory = Path.Combine(packageContentPath, "data"); - - // Check the data directory first, otherwise fall back to the older format where manifests were in the root of the package. - string workloadManifestJsonPath = Directory.Exists(dataDirectory) ? - Directory.GetFiles(dataDirectory, "WorkloadManifest.json").FirstOrDefault() : - Directory.GetFiles(packageContentPath, "WorkloadManifest.json").FirstOrDefault(); - - if (string.IsNullOrWhiteSpace(workloadManifestJsonPath)) - { - throw new FileNotFoundException($"Unable to locate WorkloadManifest.json under '{packageContentPath}'."); - } - - return workloadManifestJsonPath; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateWorkloadMsis.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateWorkloadMsis.wix.cs deleted file mode 100644 index bb866630e9f..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateWorkloadMsis.wix.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// MSBuild task to generate a set of MSIs from a set of workload manifests. - /// - public class GenerateWorkloadMsis : GenerateMsiBase - { - /// - /// The workload manifests files to process. - /// - [Required] - public ITaskItem[] WorkloadManifests - { - get; - set; - } - - /// - /// The path where the workload-pack packages referenced by the manifest files are located. - /// - public string PackagesPath - { - get; - set; - } - - /// - /// Generate msis in parallel. - /// - public bool RunInParallel - { - get; - set; - } = true; - - /// - /// Gets the set of missing workload packs. - /// - [Output] - public ITaskItem[] MissingPacks - { - get; - set; - } - - public override bool Execute() - { - try - { - List msis = new(); - List missingPacks = new(); - - if (string.IsNullOrWhiteSpace(PackagesPath)) - { - Log.LogError($"{nameof(PackagesPath)} parameter cannot be null or empty."); - return false; - } - - // Each pack maps to multiple packs and different MSI packages. We consider a pack - // to be missing when none of its dependent MSIs were found/generated. - IEnumerable workloadPacks = GetWorkloadPacks(WorkloadManifests); - - List missingPackIds = new(workloadPacks.Select(p => $"{p.Id}")); - - List<(string sourcePackage, string swixPackageId, string outputPath, WorkloadPackKind kind, string[] platforms)> packsToGenerate = new(); - - foreach (WorkloadPack pack in workloadPacks) - { - Log.LogMessage($"Processing workload pack: {pack.Id}, Version: {pack.Version}"); - - foreach ((string sourcePackage, string[] platforms) in GetSourcePackages(pack)) - { - if (!File.Exists(sourcePackage)) - { - Log?.LogMessage(MessageImportance.High, $"Workload pack package does not exist: {sourcePackage}"); - - missingPacks.Add(new TaskItem($"{pack.Id}", new Dictionary - { - { Metadata.SourcePackage, sourcePackage }, - { Metadata.Platform, string.Join(",", platforms) }, - { Metadata.ShortName, $"{pack.Id.ToString().Replace(ShortNames)}" } - })); - - continue; - } - - // Swix package is always versioned to support upgrading SxS installs. The pack alias will be - // used for individual MSIs - string swixPackageId = $"{pack.Id.ToString().Replace(ShortNames)}.{pack.Version}"; - - // Always select the pack ID for the VS MSI package, even when aliased. - packsToGenerate.Add(new(sourcePackage, swixPackageId, OutputPath, pack.Kind, platforms)); - } - } - - if (RunInParallel) - { - System.Threading.Tasks.Parallel.ForEach(packsToGenerate, p => - { - msis.AddRange(Generate(p.sourcePackage, p.swixPackageId, p.outputPath, p.kind, p.platforms)); - }); - } - else - { - foreach (var p in packsToGenerate) - { - msis.AddRange(Generate(p.sourcePackage, p.swixPackageId, p.outputPath, p.kind, p.platforms)); - } - } - - Msis = msis.ToArray(); - MissingPacks = missingPacks.ToArray(); - } - catch (Exception e) - { - Log.LogMessage(MessageImportance.Low, e.StackTrace); - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - internal static IEnumerable GetWorkloadPacks(ITaskItem[] workloadManifestItems) - { - // We need to track duplicate packs (same ID and version) so we only build MSIs once when processing - // multiple manifests. We'll manually deduplicate the packs - // since WorkloadPack doesn't provide an override for GetHashCode/Equals. - Dictionary packs = new(); - - foreach (ITaskItem item in workloadManifestItems) - { - var workloadManifest = WorkloadManifestReader.ReadWorkloadManifest( - Path.GetFileNameWithoutExtension(item.ItemSpec), File.OpenRead(item.ItemSpec), item.ItemSpec); - - foreach (var workload in workloadManifest.Workloads.Values) - { - if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(p => p.StartsWith("win"))) && (wd.Packs != null)) - { - foreach (var packId in wd.Packs) - { - var pack = workloadManifest.Packs[packId]; - string key = $"{pack.Id},{pack.Version}"; - - if (!packs.ContainsKey(key)) - { - packs[key] = pack; - } - } - } - } - } - - return packs.Values; - } - - /// - /// Gets the packages associated with a specific workload pack. - /// - /// - /// An enumerable of tuples. Each tuple contains the full path of the NuGet package and the target platforms. - internal IEnumerable<(string, string[])> GetSourcePackages(WorkloadPack pack) - { - if (pack.IsAlias) - { - foreach (string rid in pack.AliasTo.Keys) - { - string sourcePackage = Path.Combine(PackagesPath, $"{pack.AliasTo[rid]}.{pack.Version}.nupkg"); - - switch (rid) - { - case "win7": - case "win10": - case "win": - case "any": - yield return (sourcePackage, SupportedPlatforms); - break; - case "win-x64": - yield return (sourcePackage, new[] { "x64" }); - break; - case "win-x86": - yield return (sourcePackage, new[] { "x86" }); - break; - case "win-arm64": - yield return (sourcePackage, new[] { "arm64" }); - break; - default: - Log?.LogMessage($"Skipping alias ({rid})."); - continue; - } - } - } - else - { - // For non-RID specific packs we'll produce MSIs for each supported platform. - yield return (Path.Combine(PackagesPath, $"{pack.Id}.{pack.Version}.nupkg"), SupportedPlatforms); - } - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GetWorkloadPackPackageReferences.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GetWorkloadPackPackageReferences.cs deleted file mode 100644 index 4c956912fd6..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GetWorkloadPackPackageReferences.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Xml; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public class GetWorkloadPackPackageReferences : Microsoft.Build.Utilities.Task - { - public string ProjectFile - { - get; - set; - } - - public ITaskItem[] ExcludedPackIds - { - get; - set; - } - - public string PackageSource - { - get; - set; - } - - [Required] - public ITaskItem[] ManifestFiles - { - get; - set; - } - - public override bool Execute() - { - try - { - Directory.CreateDirectory(Path.GetDirectoryName(ProjectFile)); - - List packs = new(); - - var excludedPackIds = ExcludedPackIds.Select(i => i.ItemSpec); - - XmlWriterSettings settings = new XmlWriterSettings - { - Indent = true, - IndentChars = " " - }; - - XmlWriter writer = XmlWriter.Create(ProjectFile, settings); - - writer.WriteStartElement("Project"); - writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); - - writer.WriteStartElement("PropertyGroup"); - writer.WriteElementString("TargetFramework", "net6.0"); - writer.WriteElementString("IncludeBuildOutput", "false"); - writer.WriteEndElement(); - - writer.WriteStartElement("ItemGroup"); - - foreach (var manifestFile in ManifestFiles) - { - WorkloadManifest manifest = WorkloadManifestReader.ReadWorkloadManifest( - Path.GetFileNameWithoutExtension(manifestFile.ItemSpec), File.OpenRead(manifestFile.ItemSpec), manifestFile.ItemSpec); - - foreach (var pack in manifest.Packs.Values) - { - if (pack.IsAlias) - { - foreach (var alias in pack.AliasTo.Keys.Where(k => k.StartsWith("win"))) - { - if (!excludedPackIds.Contains($"{pack.AliasTo[alias]}")) - { - WriteItem(writer, "PackageDownload", ("Include", $"{pack.AliasTo[alias]}"), ("Version", $"[{pack.Version}]")); - packs.Add($"$(NuGetPackageRoot){pack.AliasTo[alias]}\\{pack.Version}\\*.nupkg"); - } - } - } - else if (!excludedPackIds.Contains($"{pack.Id}")) - { - WriteItem(writer, "PackageDownload", ("Include", $"{pack.Id}"), ("Version", $"[{pack.Version}]")); - packs.Add($"$(NuGetPackageRoot){pack.Id}\\{pack.Version}\\*.nupkg"); - } - } - } - - writer.WriteEndElement(); - - writer.WriteStartElement("Target"); - writer.WriteAttributeString("Name", "CopyPacks"); - writer.WriteAttributeString("AfterTargets", "Build"); - - writer.WriteStartElement("ItemGroup"); - - foreach (var pack in packs) - { - WriteItem(writer, "Pack", ("Include", pack)); - } - - writer.WriteEndElement(); - - writer.WriteStartElement("Copy"); - writer.WriteAttributeString("SourceFiles", "@(Pack)"); - writer.WriteAttributeString("DestinationFolder", $"{PackageSource}"); - writer.WriteEndElement(); - - writer.WriteEndElement(); - - writer.WriteEndElement(); - writer.Flush(); - writer.Close(); - } - catch (Exception e) - { - Log.LogErrorFromException(e); - } - - return !Log.HasLoggedErrors; - } - - private void WriteItem(XmlWriter writer, string itemName, params (string name, string value)[] metadata) - { - writer.WriteStartElement(itemName); - - foreach (var m in metadata) - { - writer.WriteAttributeString(m.name, m.value); - } - - writer.WriteEndElement(); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ICollectionExtensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ICollectionExtensions.cs deleted file mode 100644 index bac41db388f..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ICollectionExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Extension methods for ICollection. - /// - public static class ICollectionExtensions - { - /// - /// Adds the elements of the source collection to the end of the destination collection. - /// - /// - /// The collection to modify. - /// The collection to add. - public static void AddRange(this ICollection destination, IEnumerable source) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - foreach (T item in source) - { - destination.Add(item); - } - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/LibraryPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/LibraryPackPackage.wix.cs new file mode 100644 index 00000000000..2bda800d015 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/LibraryPackPackage.wix.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a library pack. + /// + internal class LibraryPackPackage : WorkloadPackPackage + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Copy; + + public LibraryPackPackage(WorkloadPack pack, string packagePath, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(pack, packagePath, platforms, destinationBaseDirectory, shortNames, log) + { + + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs index 2c5e220fcce..664e06b2d7e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs @@ -3,20 +3,34 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads { - internal class Metadata + /// + /// Metadata names for MSBuild ITaskItem. + /// + internal static class Metadata { - public const string AliasTo = "AliasTo"; - public const string Category = "Category"; - public const string Description = "Description"; - public const string JsonProperties = "JsonProperties"; - public const string Platform = "Platform"; - public const string Replacement = "Replacement"; - public const string PackageProject = "PackageProject"; - public const string ShortName = "ShortName"; - public const string SourcePackage = "SourcePackage"; - public const string SwixProject = "SwixProject"; - public const string Title = "Title"; - public const string Version = "Version"; - public const string WixObj = "WixObj"; + public static readonly string AliasTo = nameof(AliasTo); + public static readonly string Category = nameof(Category); + public static readonly string Description = nameof(Description); + public static readonly string Filename = nameof(Filename); + public static readonly string FullPath = nameof(FullPath); + public static readonly string JsonProperties = nameof(JsonProperties); + public static readonly string MsiVersion = nameof(MsiVersion); + public static readonly string Platform = nameof(Platform); + public static readonly string RelativeDir = nameof(RelativeDir); + public static readonly string Replacement = nameof(Replacement); + public static readonly string PackageProject = nameof(PackageProject); + public static readonly string SdkFeatureBand = nameof(SdkFeatureBand); + public static readonly string ShortName = nameof(ShortName); + public static readonly string SourcePackage = nameof(SourcePackage); + public static readonly string SwixPackageId = nameof(SwixPackageId); + public static readonly string SwixProject = nameof(SwixProject); + public static readonly string Title = nameof(Title); + public static readonly string Version = nameof(Version); + + /// + /// Metadata used by tasks generating MSIs to specify the path of .wixobj files produced by + /// the compiler. + /// + public static readonly string WixObj = nameof(WixObj); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj index ec8b207869a..bcb8b9b9d35 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj @@ -1,4 +1,4 @@ - + net472;$(TargetFrameworkForNETSDK) @@ -61,16 +61,31 @@ + - - + + + + + + True + True + Strings.resx + + + + + + ResXFileCodeGenerator + Strings.Designer.cs + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj new file mode 100644 index 00000000000..549eecf737a --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj @@ -0,0 +1,42 @@ + + + + <_Authors>__AUTHORS__ + <_Copyright>__COPYRIGHT__ + <_PackageProjectUrl>__PACKAGE_PROJECT_URL__ + + + + $(_Authors) + $(_Copyright) + __DESCRIPTION__ + true + false + true + $(NoWarn);NU5128 + __PACKAGE_ID__ + MIT + $(_PackageProjectUrl) + DotnetPlatform + __PACKAGE_VERSION__ + true + net5.0 + + + + + + + + + + + Icon.png + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs new file mode 100644 index 00000000000..30fac6f1b1f --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.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. + +using Microsoft.Deployment.WindowsInstaller; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines a single row inside the File table of an MSI. + /// + public class FileRow + { + /// + /// An integer containing bit flags describing various file attributes. + /// + public int Attributes + { + get; + set; + } + + /// + /// The external key into the Component table. + /// + public string Component_ + { + get; + set; + } + + /// + /// A non-localized token that uniquely identifies the file. + /// + public string File + { + get; + set; + } + + /// + /// The file name used for installation. + /// + public string FileName + { + get; + set; + } + + /// + /// The size of the file in bytes. + /// + public int FileSize + { + get; + set; + } + + /// + /// A comma separated list of decimal language IDs. + /// + public string Language + { + get; + set; + } + + /// + /// The sequence position of this file on the media images. + /// + public int Sequence + { + get; + set; + } + + /// + /// A string containing the version. The value may be empty for a non-versioned file. + /// + public string Version + { + get; + set; + } + + /// + /// Creates a new instance from the specified . + /// + /// The file record obtained from querying the MSI File table. + /// A single file row. + public static FileRow Create(Record fileRecord) + { + return new FileRow + { + Attributes = (int)fileRecord["Attributes"], + Component_ = (string)fileRecord["Component_"], + File = (string)fileRecord["File"], + FileName = (string)fileRecord["FileName"], + FileSize = (int)fileRecord["FileSize"], + Language = (string)fileRecord["Language"], + Sequence = (int)fileRecord["Sequence"], + Version = (string)fileRecord["Version"], + }; + } + } +} 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 new file mode 100644 index 00000000000..91fb78e1664 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -0,0 +1,222 @@ +// 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.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + internal abstract class MsiBase + { + /// + /// Replacement token for license URLs in the generated EULA. + /// + private static readonly string __LICENSE_URL__ = nameof(__LICENSE_URL__); + + /// + /// Static RTF text for inserting a EULA into the MSI. The license URL of the NuGet package will be embedded + /// as plain text since the text control used to render the MSI UI does not render hyperlinks even though RTF supports + /// it. + /// + internal static readonly string s_eula = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} +{\colortbl ;\red0\green0\blue255;} +{\*\generator Riched20 10.0.19041}\viewkind4\uc1 +\pard\sa200\sl276\slmult1\f0\fs22\lang9 This software is licensed separately as set out in its accompanying license. By continuing, you also agree to that license (__LICENSE_URL__).\par +\par +}"; + /// + /// The UUID namesapce to use for generating an upgrade code. + /// + internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C"); + + /// + /// The workload package used to create the MSI. + /// + public WorkloadPackageBase Package + { + get; + } + + protected IBuildEngine BuildEngine + { + get; + } + + /// + /// The directory where the compiler output (.wixobj files) will be generated. + /// + protected string CompilerOutputPath + { + get; + } + + /// + /// The root intermediate output directory. + /// + protected string BaseIntermediateOutputPath + { + get; + } + + /// + /// Gets the value to use for the manufacturer. + /// + protected string Manufacturer => + (!string.IsNullOrWhiteSpace(Package.Authors) && (Package.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) ? + Package.Authors : + DefaultValues.Manufacturer; + + /// + /// The platform of the MSI. + /// + protected string Platform + { + get; + } + + /// + /// The directory where the WiX source code will be generated. + /// + protected string WixSourceDirectory + { + get; + } + + /// + /// The path w + /// + protected string WixToolsetPath + { + get; + } + + public MsiBase(WorkloadPackageBase package, IBuildEngine buildEngine, string wixToolsetPath, + string platform, string baseIntermediateOutputPath) + { + BuildEngine = buildEngine; + WixToolsetPath = wixToolsetPath; + Platform = platform; + BaseIntermediateOutputPath = baseIntermediateOutputPath; + + // Candle expects the output path to be terminated with a single '\'. + CompilerOutputPath = Utils.EnsureTrailingSlash(Path.Combine(baseIntermediateOutputPath, "wixobj", package.Id, $"{package.PackageVersion}", platform)); + WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", package.Id, $"{package.PackageVersion}", platform); + Package = package; + } + + /// + /// Produces an MSI and returns a task item with metadata about the MSI. + /// + /// The directory where the MSI will be generated. + /// A set of internal consistency evaluators to suppress or . + /// An item representing the built MSI. + public abstract ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions); + + /// + /// Gets the platform specific ProductName MSI property. + /// + /// The platform targeted by the MSI. + /// A string containing the product name of the MSI. + protected string GetProductName(string platform) => + (string.IsNullOrWhiteSpace(Package.Title) ? Package.Id : Package.Title) + $" ({platform})"; + + /// + /// Generates a EULA (RTF file) that contains the license URL of the underlying NuGet package. + /// + protected string GenerateEula() + { + string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf"); + File.WriteAllText(eulaRtf, s_eula.Replace(__LICENSE_URL__, Package.LicenseUrl)); + + return eulaRtf; + } + + /// + /// Creates a new compiler tool task and configures some common extensions and preprocessor + /// variables. + /// + /// + protected CompilerToolTask CreateDefaultCompiler() + { + CompilerToolTask candle = new(BuildEngine, WixToolsetPath, CompilerOutputPath, Platform); + + // Required extension to parse the dependency provider authoring. + candle.AddExtension(WixExtensions.WixDependencyExtension); + + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Package.Id); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Package.PackageVersion}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid()}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform)); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Package.MsiVersion}"); + + return candle; + } + + /// + /// Links the MSI using the output from the WiX compiler using a default set of WiX extensions. + /// + /// The path where the output of the compiler (.wixobj files) will be generated. + /// The full path of the MSI to create during linking. + /// A set of internal consistency evaluators to suppress. May be . + /// An for the MSI that was created. + /// + protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions = null) + { + return Link(compilerOutputPath, outputFile, iceSuppressions, WixExtensions.WixDependencyExtension, + WixExtensions.WixUIExtension, WixExtensions.WixUtilExtension); + } + + /// + /// Links the MSI using the output from the WiX compiler and a set of WiX extensions. + /// + /// The path where the output of the compiler (.wixobj files) can be found. + /// The full path of the MSI to create during linking. + /// A set of internal consistency evaluators to suppress. May be . + /// A list of WiX extensions to include when linking the MSI. + /// An for the MSI that was created. + /// + protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions, params string[] wixExtensions) + { + // Link the MSI. The generated filename contains the semantic version (excluding build metadata) and platform. + // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add + // the platform again. + LinkerToolTask light = new(BuildEngine, WixToolsetPath) + { + OutputFile = outputFile, + SourceFiles = Directory.EnumerateFiles(compilerOutputPath, "*.wixobj"), + SuppressIces = iceSuppressions == null ? null : string.Join(";", iceSuppressions.Select(i => i.ItemSpec)) + }; + + foreach (string wixExtension in wixExtensions) + { + light.AddExtension(wixExtension); + } + + if (!light.Execute()) + { + throw new Exception(Strings.FailedToLinkMsi); + } + + TaskItem msiItem = new TaskItem(light.OutputFile); + + // Return a task item that contains all the information about the generated MSI. + msiItem.SetMetadata(Metadata.Platform, Platform); + msiItem.SetMetadata(Metadata.WixObj, compilerOutputPath); + msiItem.SetMetadata(Metadata.Version, $"{Package.MsiVersion}"); + msiItem.SetMetadata(Metadata.SwixPackageId, Package.SwixPackageId); + + return msiItem; + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs new file mode 100644 index 00000000000..45a83180c29 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs @@ -0,0 +1,59 @@ +// 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.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Describes a project to package an MSI and its JSON manifest into a NuGet package. + /// + internal class MsiPayloadPackageProject : ProjectTemplateBase + { + /// + protected override string ProjectSourceDirectory + { + get; + } + + /// + protected override string ProjectFile + { + get; + } + + public MsiPayloadPackageProject(WorkloadPackageBase package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, string msiJsonPath) : + base(baseIntermediateOutputPath, baseOutputPath) + { + string platform = msi.GetMetadata(Metadata.Platform); + ProjectSourceDirectory = Path.Combine(SourceDirectory, "msiPackage", platform, package.Id); + ProjectFile = "msi.csproj"; + + ReplacementTokens[PayloadPackageTokens.__AUTHORS__] = package.Authors; + ReplacementTokens[PayloadPackageTokens.__COPYRIGHT__] = package.Copyright; + ReplacementTokens[PayloadPackageTokens.__DESCRIPTION__] = package.Description; + ReplacementTokens[PayloadPackageTokens.__PACKAGE_ID__] = $"{package.Id}.Msi.{platform}"; + ReplacementTokens[PayloadPackageTokens.__PACKAGE_PROJECT_URL__] = package.ProjectUrl; + ReplacementTokens[PayloadPackageTokens.__PACKAGE_VERSION__] = $"{package.PackageVersion}"; + ReplacementTokens[PayloadPackageTokens.__MSI__] = msi.GetMetadata(Metadata.FullPath); + ReplacementTokens[PayloadPackageTokens.__MSI_JSON__] = msiJsonPath; + ReplacementTokens[PayloadPackageTokens.__LICENSE_FILENAME__] = "LICENSE.TXT"; + } + + /// + public override string Create() + { + string msiCsproj = EmbeddedTemplates.Extract("msi.csproj", ProjectSourceDirectory); + + Utils.StringReplace(msiCsproj, ReplacementTokens, Encoding.UTF8); + EmbeddedTemplates.Extract("Icon.png", ProjectSourceDirectory); + EmbeddedTemplates.Extract("LICENSE.TXT", ProjectSourceDirectory); + + return msiCsproj; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperties.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperties.wix.cs new file mode 100644 index 00000000000..6d0e72b180f --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperties.wix.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines MSI properties that are published to a JSON manifest included in a payload package. + /// This avoids performing expensive queries against the MSI at install time from the CLI. + /// + public class MsiProperties + { + /// + /// The size, in bytes, required to install the MSI. + /// + public long InstallSize + { + get; + set; + } + + /// + /// The ProductLanguage property. + /// + public int Language + { + get; + set; + } + + /// + /// The MSI payload file. + /// + public string Payload + { + get; + set; + } + + /// + /// The MSI ProductCode. + /// + public string ProductCode + { + get; + set; + } + + /// + /// The MSI ProductVersion. + /// + public string ProductVersion + { + get; + set; + } + + /// + /// The MSI dependency provider key name used to manage reference counts for the MSI. + /// + public string ProviderKeyName + { + get; + set; + } + + /// + /// The MSI UpgradeCode. + /// + public string UpgradeCode + { + get; + set; + } + + /// + /// An enumerable set of all the rows from the MSI's Upgrade table. The information is used + /// to determine when to apply or ignore upgrades. + /// + public IEnumerable RelatedProducts + { + get; + set; + } + + /// + /// Creates JSON manifest describing an MSI payload. + /// + /// The path to the MSI package. + /// A string containing the ProductLanguage, expressed as a decimal value, e.g. 1033. If , the property will be read from the MSI. + /// A string containing the ProductCode GUID. If , the property will be read from the MSI. + /// A string containing the ProductVersion. If , the property will be read from the MSI. + /// The name of the dependency provider key. If , the property will be read from the MSI. + /// A string containing the UpgradeCode GUID. If , the property will be read from the MSI. + /// The path to the JSON manifest. + public static string Create(string path, string productLanguage = null, string productCode = null, string productVersion = null, + string providerKeyName = null, string upgradeCode = null) + { + MsiProperties properties = new() + { + InstallSize = MsiUtils.GetInstallSize(path), + Language = Convert.ToInt32(productLanguage == null ? MsiUtils.GetProperty(path, MsiProperty.ProductLanguage) : productLanguage), + Payload = Path.GetFileName(path), + ProductCode = productCode == null ? MsiUtils.GetProperty(path, MsiProperty.ProductCode) : productCode, + ProductVersion = productVersion == null ? MsiUtils.GetProperty(path, MsiProperty.ProductVersion) : productVersion, + ProviderKeyName = providerKeyName == null ? MsiUtils.GetProviderKeyName(path) : providerKeyName, + UpgradeCode = upgradeCode == null ? MsiUtils.GetProperty(path, MsiProperty.UpgradeCode) : upgradeCode, + RelatedProducts = MsiUtils.GetRelatedProducts(path) + }; + + string msiJsonPath = Path.ChangeExtension(path, ".json"); + File.WriteAllText(msiJsonPath, JsonSerializer.Serialize(properties)); + + return msiJsonPath; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperty.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperty.wix.cs new file mode 100644 index 00000000000..a26707213f4 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiProperty.wix.cs @@ -0,0 +1,17 @@ +// 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 MSI property names that can be used to query the Property table. + /// + internal static class MsiProperty + { + public static readonly string ProductCode = nameof(ProductCode); + public static readonly string ProductLanguage = nameof(ProductLanguage); + public static readonly string ProductName = nameof(ProductName); + public static readonly string ProductVersion = nameof(ProductVersion); + public static readonly string UpgradeCode = nameof(UpgradeCode); + } +} 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 new file mode 100644 index 00000000000..2eb34edbfe6 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs @@ -0,0 +1,133 @@ +// 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 Microsoft.Deployment.WindowsInstaller; +using Microsoft.Deployment.WindowsInstaller.Package; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Utility methods for Windows Installer (MSI) packages. + /// + public class MsiUtils + { + /// + /// Query string to retrieve all the rows from the MSI File table. + /// + private const string _getFilesQuery = "SELECT `File`, `Component_`, `FileName`, `FileSize`, `Version`, `Language`, `Attributes`, `Sequence` FROM `File`"; + + /// + /// Query string to retrieve all the rows from the MSI Upgrade table. + /// + private const string _getUpgradeQuery = "SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes` FROM `Upgrade`"; + + /// + /// Query string to retrieve the dependency provider key from the WixDependencyProvider table. + /// + private const string _getWixDependencyProviderQuery = "SELECT `ProviderKey` FROM `WixDependencyProvider`"; + + /// + /// Gets an enumeration of all the files inside an MSI. + /// + /// The path of the MSI package to query. + /// An enumeration of all the files. + public static IEnumerable GetAllFiles(string packagePath) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + using View fileView = db.OpenView(_getFilesQuery); + List files = new(); + fileView.Execute(); + + foreach (Record fileRecord in fileView) + { + files.Add(FileRow.Create(fileRecord)); + } + + return files; + } + + /// + /// Gets an enumeration describing related products defined in the Upgrade table of an MSI + /// + /// The path of the MSI package to query. + /// An enumeration of upgrade related products. + public static IEnumerable GetRelatedProducts(string packagePath) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + + if (db.Tables.Contains("Upgrade")) + { + using View upgradeView = db.OpenView(_getUpgradeQuery); + List relatedProducts = new(); + upgradeView.Execute(); + + foreach (Record relatedProduct in upgradeView) + { + relatedProducts.Add(RelatedProduct.Create(relatedProduct)); + } + + return relatedProducts; + } + + return Enumerable.Empty(); + } + + /// + /// Gets the dependency provider key from the MSI package. + /// + /// The path of the MSI package to query. + /// The name of the provider key or if the WixDependencyProvider table does not exist. + public static string GetProviderKeyName(string packagePath) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + + if (db.Tables.Contains("WixDependencyProvider")) + { + using View depProviderView = db.OpenView(_getWixDependencyProviderQuery); + depProviderView.Execute(); + + Record providerKey = depProviderView.First(); + + return providerKey != null ? (string)providerKey["ProviderKey"] : null; + } + + return null; + } + + /// + /// Extracts the specified property from the MSI Property table. + /// + /// The path to the MSI package. + /// The name of the property to extract. + /// The value of the property. + public static string GetProperty(string packagePath, string property) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + return ip.Property[property]; + } + + /// + /// Gets the ProductVersion property of the specified MSI. + /// + /// The path to the MSI package. + /// The ProductVersion property. + public static Version GetVersion(string packagePath) => + new Version(GetProperty(packagePath, MsiProperty.ProductVersion)); + + /// + /// Calculates the number of bytes a Windows Installer Package would consume on disk. The function assumes that all files will be installed. + /// + /// The path to the MSI package. + /// Multiplication factor to use to account for additional space requirements such as registry entries for components + /// in the installer database. + /// 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)); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs new file mode 100644 index 00000000000..13755c1c448 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines token names used to create an MSI payload package (NuGet). + /// + internal static class PayloadPackageTokens + { + public static readonly string __AUTHORS__ = nameof(__AUTHORS__); + public static readonly string __COPYRIGHT__ = nameof(__COPYRIGHT__); + public static readonly string __DESCRIPTION__ = nameof(__DESCRIPTION__); + public static readonly string __LICENSE_FILENAME__ = nameof(__LICENSE_FILENAME__); + public static readonly string __MSI__ = nameof(__MSI__); + public static readonly string __MSI_JSON__ = nameof(__MSI_JSON__); + public static readonly string __PACKAGE_ID__ = nameof(__PACKAGE_ID__); + public static readonly string __PACKAGE_PROJECT_URL__ = nameof(__PACKAGE_PROJECT_URL__); + public static readonly string __PACKAGE_VERSION__ = nameof(__PACKAGE_VERSION__); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/RelatedProduct.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs similarity index 64% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/RelatedProduct.wix.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs index 4eb3a09ba1f..3619e87e3d1 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/RelatedProduct.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs @@ -1,41 +1,57 @@ // 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 Microsoft.Deployment.WindowsInstaller; -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { - // Represents a single row from the MSI Upgrade table. + /// + /// Represents a single row from the MSI Upgrade table. + /// public class RelatedProduct { + /// + /// The UpgradeCode of the related product. + /// public string UpgradeCode { get; set; } - + + /// + /// The minimum version of the related product. + /// public string VersionMin { get; set; } + /// + /// The maximum version of the related product. + /// public string VersionMax { get; set; } + /// + /// A comma separate list of decimal language identifiers. + /// public string Language { get; set; } + /// + /// An integer containing bit flags describing attributes of the Upgrade table. + /// public int Attributes { - get; + get; set; } 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 new file mode 100644 index 00000000000..a0714d20a68 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -0,0 +1,93 @@ +// 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.IO; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Represents a workload manifest MSI. + /// + internal class WorkloadManifestMsi : MsiBase + { + private WorkloadManifestPackage _package; + + /// + /// The directory reference to use when harvesting the package contents. + /// + private static readonly string ManifestIdDirectory = "ManifestIdDir"; + + public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, + string baseIntermediateOutputPath) : + base(package, buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath) + { + _package = package; + } + + /// + /// + public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null) + { + // Harvest the package contents before adding it to the source files we need to compile. + string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs"); + string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); + + HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) + { + DirectoryReference = ManifestIdDirectory, + 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("dotnethome_x64.wxs", WixSourceDirectory), + EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory)); + + // Only extract the include file as it's not compilable, but imported by various source files. + EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); + + // To support upgrades, the UpgradeCode must be stable within an SDK feature band. + // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. + // The workload author will need to guarantee that the version for the MSI is higher than previous shipped versions + // to ensure upgrades correctly trigger. + Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.ManifestId};{_package.SdkFeatureBand};{Platform}"); + string providerKeyName = $"{_package.ManifestId},{_package.SdkFeatureBand},{Platform}"; + + // Set up additional preprocessor definitions. + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}"); + + // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. + // We have to do the same to ensure the keypath generation produces stable GUIDs. + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{_package.ManifestId.ToLowerInvariant()}"); + + if (!candle.Execute()) + { + throw new Exception(Strings.FailedToCompileMsi); + } + + ITaskItem msi = Link(candle.OutputPath, + Path.Combine(outputPath, Path.GetFileNameWithoutExtension(_package.PackagePath) + $"-{Platform}.msi"), + iceSuppressions); + + return msi; + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs new file mode 100644 index 00000000000..32a82891192 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -0,0 +1,100 @@ +// 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.IO; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + internal class WorkloadPackMsi : MsiBase + { + private WorkloadPackPackage _package; + + public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, + string baseIntermediatOutputPath) : + base(package, buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + { + _package = package; + } + + public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null) + { + // Harvest the package contents before adding it to the source files we need to compile. + string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs"); + string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? + "InstallDir" : "VersionDir"; + + HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) + { + DirectoryReference = directoryReference, + OutputFile = packageContentWxs, + Platform = this.Platform, + SourceDirectory = _package.DestinationDirectory + }; + + if (!heat.Execute()) + { + throw new Exception(Strings.HeatFailedToHarvest); + } + + CompilerToolTask candle = CreateDefaultCompiler(); + + candle.AddSourceFiles(packageContentWxs, + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory), + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory), + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory), + EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory), + EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory)); + + // Only extract the include file as it's not compilable, but imported by various source files. + EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); + + // Workload packs are not upgradable so the upgrade code is generated using the package identity as that + // includes the package version. + Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); + string providerKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}"; + + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); + + if (!candle.Execute()) + { + throw new Exception(Strings.FailedToCompileMsi); + } + + // Add the platform to the MSI unless the package name already contains it. + string msiFileName = _package.PackageFileName.Contains(Platform) ? + Path.Combine(outputPath, _package.ShortName + ".msi") : + Path.Combine(outputPath, _package.ShortName + $"-{Platform}.msi"); + + ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + + return msi; + } + + /// + /// Get the installation directory based on the kind of workload pack. + /// + /// The workload pack kind. + /// The name of the root installation directory. + internal static string GetInstallDir(WorkloadPackKind kind) => + kind switch + { + WorkloadPackKind.Framework or WorkloadPackKind.Sdk => "packs", + WorkloadPackKind.Library => "library-packs", + WorkloadPackKind.Template => "template-packs", + WorkloadPackKind.Tool => "tool-packs", + _ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, kind)), + }; + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiPackage.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiPackage.cs deleted file mode 100644 index 1cfb7998e70..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiPackage.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; -using NuGet.Versioning; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Represents an MSI package. - /// - public class MsiPackage - { - public string InstallDir - { - get; - } - - public string OutputFile - { - get; - } - - /// - /// The target platform of the MSI. - /// - public string Platform - { - get; - } - - public string SourcePackage - { - get; - } - - public MsiPackage(string sourcePackage, string platform, string installDir) - { - SourcePackage = sourcePackage; - Platform = platform; - InstallDir = installDir; - } - - public static IEnumerable Create(string sourcePackage, string installDir, params string[] platforms) - { - foreach (string platform in platforms) - { - yield return new MsiPackage(sourcePackage, installDir, platform); - } - } - - - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiProperties.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiProperties.wix.cs deleted file mode 100644 index 7c8376c69a0..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiProperties.wix.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public class MsiProperties - { - public long InstallSize - { - get; - set; - } - - public int Language - { - get; - set; - } - - public string Payload - { - get; - set; - } - - public string ProductCode - { - get; - set; - } - - public string ProductVersion - { - get; - set; - } - - public string ProviderKeyName - { - get; - set; - } - - public string UpgradeCode - { - get; - set; - } - - public IEnumerable RelatedProducts - { - get; - set; - } - } -} 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 4537bf65bcb..13c2eadf30f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -45,8 +45,8 @@ - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs index 6f628f7797b..43b36f3ce2c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs @@ -13,8 +13,8 @@ - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiUtils.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiUtils.wix.cs deleted file mode 100644 index 6ca695ff84f..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiUtils.wix.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Deployment.WindowsInstaller; -using Microsoft.Deployment.WindowsInstaller.Package; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - public class MsiUtils - { - private const string _getFilesQuery = "SELECT `File`, `Component_`, `FileName`, `FileSize`, `Version`, `Language`, `Attributes`, `Sequence` FROM `File`"; - - private const string _getUpgradeQuery = "SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes` FROM `Upgrade`"; - - public static IEnumerable GetAllFiles(string packagePath) - { - using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); - using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); - using View fileView = db.OpenView(_getFilesQuery); - List files = new(); - fileView.Execute(); - - foreach (Record file in fileView) - { - files.Add(FileRow.Create(file, ip.Files[(string)file["File"]].TargetPath)); - } - - return files; - } - - public static IEnumerable GetRelatedProducts(string packagePath) - { - using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); - using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); - - if (db.Tables.Contains("Upgrade")) - { - using View upgradeView = db.OpenView(_getUpgradeQuery); - List relatedProducts = new(); - upgradeView.Execute(); - - foreach (Record relatedProduct in upgradeView) - { - relatedProducts.Add(RelatedProduct.Create(relatedProduct)); - } - - return relatedProducts; - } - - return Enumerable.Empty(); - } - - /// - /// Extracts the specified property from the MSI's Property table. - /// - /// The path to the MSI package. - /// The name of the property to extract. - /// The value of the property. - public static string GetProperty(string packagePath, string property) - { - using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); - return ip.Property[property]; - } - - /// - /// Calculate the number of bytes a Windows Installer Package would consume on disk. The function assumes that all files will be installed. - /// - /// The path of the installation package. - /// Multiplication factor to use to account for additional space requirements, e.g. registry entries for components in the installer database. - /// - public static long GetInstallSize(string packagePath, double factor = 1.4) - { - var files = GetAllFiles(packagePath); - return files.Sum(f => Convert.ToInt64(f.FileSize * factor)); - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/NuGetPackage.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/NuGetPackage.cs deleted file mode 100644 index 20f4c8ba06a..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/NuGetPackage.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using NuGet.Packaging; -using NuGet.Packaging.Core; -using NuGet.Versioning; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Represents a NuGet package that can be harvested to generate an MSI. - /// - public class NugetPackage - { - /// - /// The package authors. - /// - public string Authors - { - get; - } - - public string Copyright - { - get; - } - - public string Description - { - get; - } - - /// - /// The NuGet package identifier. - /// - public string Id => Identity.Id; - - /// - /// The identity of the NuGet package. - /// - public PackageIdentity Identity - { - get; - } - - public LicenseMetadata LicenseData - { - get; - } - - public string LicenseUrl - { - get; - } - - public string PackagePath - { - get; - } - - public string ProductVersion => $"{Version.Major}.{Version.Minor}.{Version.Patch}"; - - public string Title - { - get; - } - - public string ProjectUrl - { - get; - } - - /// - /// The version of the NuGet package. - /// - public NuGetVersion Version => Identity.Version; - - private TaskLoggingHelper Log; - - public NugetPackage(string packagePath, TaskLoggingHelper log) - { - Utils.CheckNullOrEmpty(nameof(packagePath), packagePath); - PackagePath = packagePath; - Log = log; - - using FileStream packageFileStream = new(PackagePath, FileMode.Open); - using PackageArchiveReader packageReader = new(packageFileStream); - NuspecReader nuspecReader = new(packageReader.GetNuspec()); - - Identity = nuspecReader.GetIdentity(); - Title = nuspecReader.GetTitle(); - Authors = nuspecReader.GetAuthors(); - LicenseUrl = nuspecReader.GetLicenseUrl(); - Description = nuspecReader.GetDescription(); - Copyright = nuspecReader.GetCopyright(); - LicenseData = nuspecReader.GetLicenseMetadata(); - ProjectUrl = nuspecReader.GetProjectUrl(); - } - - /// - /// Extract the package contents to the specified directory. Standard metadata will be deleted, e.g. _rels folder, .nuspec file, etc. - /// - /// The directory where the package will be extracted. - public void Extract(string destinationDirectory, IEnumerable exclusionPatterns) - { - if (Directory.Exists(destinationDirectory)) - { - Directory.Delete(destinationDirectory, recursive: true); - } - Directory.CreateDirectory(destinationDirectory); - ZipFile.ExtractToDirectory(PackagePath, destinationDirectory); - - // Remove unnecessary files and directories - Utils.DeleteDirectory(Path.Combine(destinationDirectory, "_rels")); - Utils.DeleteDirectory(Path.Combine(destinationDirectory, "package")); - - Utils.DeleteFile(Path.Combine(destinationDirectory, ".signature.p7s")); - Utils.DeleteFile(Path.Combine(destinationDirectory, "[Content_Types].xml")); - Utils.DeleteFile(Path.Combine(destinationDirectory, $"{Id}.nuspec")); - - if (exclusionPatterns.Count() > 0) - { - IEnumerable allFiles = Directory.EnumerateFiles(destinationDirectory, "*.*", SearchOption.AllDirectories); - IEnumerable filesToDelete = allFiles.Where(f => exclusionPatterns.Any(p => Regex.IsMatch(f, p))); - - Log?.LogMessage(MessageImportance.High, $"Found {filesToDelete.Count()} files matching exclusion patterns."); - - foreach (string file in filesToDelete) - { - Log?.LogMessage(MessageImportance.High, $"Deleting '{file}'."); - File.Delete(file); - } - } - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/PackageExtractionMethod.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/PackageExtractionMethod.cs new file mode 100644 index 00000000000..7753c52fa8d --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/PackageExtractionMethod.cs @@ -0,0 +1,23 @@ +// 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 +{ + /// + /// Describes how a workload pack's contents should be handled when + /// creating an installer from the underlying NuGet package. + /// + public enum PackageExtractionMethod + { + /// + /// The package contents is extracted and package metadata files and folders will be removed. + /// + Unzip = 0, + + /// + /// The package contents is not extracted. The underlying package will be copied to the destination + /// location instead. + /// + Copy = 1, + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs new file mode 100644 index 00000000000..373441732f3 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ProjectTemplateBase.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Base class used to create projects that produce workload related artifacts. + /// + public abstract class ProjectTemplateBase + { + /// + /// The root output directory. + /// + public string BaseOutputPath + { + get; + set; + } + + /// + /// The root intermediate output directory. + /// + public string BaseIntermediateOutputPath + { + get; + set; + } + + /// + /// The filename and extension of the generated project. + /// + protected abstract string ProjectFile + { + get; + } + + /// + /// The directory where the project source is generated. + /// + protected abstract string ProjectSourceDirectory + { + get; + } + + protected Dictionary ReplacementTokens + { + get; + } = new(); + + /// + /// The root directory for generated source files. + /// + public string SourceDirectory => Path.Combine(BaseIntermediateOutputPath, "src"); + + public ProjectTemplateBase(string baseIntermediateOutputPath, string baseOutputPath) + { + BaseIntermediateOutputPath = baseIntermediateOutputPath; + BaseOutputPath = baseOutputPath; + } + + /// + /// Generates the project template and returns the path to the project file. + /// + /// The path to the project file. + public abstract string Create(); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SdkPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SdkPackPackage.wix.cs new file mode 100644 index 00000000000..e7960846dc2 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SdkPackPackage.wix.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents an SDK workload pack. + /// + internal class SdkPackPackage : WorkloadPackPackage + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; + + public SdkPackPackage(WorkloadPack pack, string packagePath, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(pack, packagePath, platforms, destinationBaseDirectory, shortNames, log) + { + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/StringExtensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/StringExtensions.cs index a48d04655bb..54d9445aa4a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/StringExtensions.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/StringExtensions.cs @@ -2,23 +2,30 @@ // 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.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.Build.Tasks; namespace Microsoft.DotNet.Build.Tasks.Workloads { public static class StringExtensions { + /// + /// Removes the leading occurence of a string from the current string. + /// + /// The current string. + /// The string to remove. + /// The string that remains after the leading occurence of was removed. public static string TrimStart(this string str, string trimString) { return str.StartsWith(trimString) ? str.Remove(0, trimString.Length) : str; } + /// + /// Removes the leading occrence of a string from the current string. + /// + /// The current string. + /// The string to remove. + /// Specifies the comparison rules to use when searching for the string to remove. + /// The string that remains after the leading occurence of was removed. public static string TrimStart(this string str, string trimString, StringComparison comparisonType) { return str.StartsWith(trimString, comparisonType) ? str.Remove(0, trimString.Length) : str; @@ -27,9 +34,9 @@ public static string TrimStart(this string str, string trimString, StringCompari /// /// Replace multiple substrings using task items. /// - /// + /// The current string. /// An array of task items containing substrings and replacement strings. - /// + /// A string with all instances of the specified strings have been replaced. public static string Replace(this string str, ITaskItem[] replacementStrings) { if ((replacementStrings is not null) && (replacementStrings.Length > 0)) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs new file mode 100644 index 00000000000..9c07f47fddd --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs @@ -0,0 +1,234 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.DotNet.Build.Tasks.Workloads { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.DotNet.Build.Tasks.Workloads.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Extracting package: {0}.. + /// + internal static string BuildExtractingPackage { + get { + return ResourceManager.GetString("BuildExtractingPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to extract the manifest ID from the package ID: {0}.. + /// + internal static string CannotExtractManifestIdFromPackageId { + get { + return ResourceManager.GetString("CannotExtractManifestIdFromPackageId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to extract the SDK version from the package ID: {0}.. + /// + internal static string CannotExtractSdkVersionFromPackageId { + get { + return ResourceManager.GetString("CannotExtractSdkVersionFromPackageId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Components cannot have a null description. Either provide a custom resource or add a description to the workload definition.. + /// + internal static string ComponentDescriptionCannotBeNull { + get { + return ResourceManager.GetString("ComponentDescriptionCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A SWIX component must have at least one dependency, Id: {0}.. + /// + internal static string ComponentMustHaveAtLeastOneDependency { + get { + return ResourceManager.GetString("ComponentMustHaveAtLeastOneDependency", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Components cannot have a null title. Either provide a custom resource or add a description to the workload definition.. + /// + internal static string ComponentTitleCannotBeNull { + get { + return ResourceManager.GetString("ComponentTitleCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to compile MSI. . + /// + internal static string FailedToCompileMsi { + get { + return ResourceManager.GetString("FailedToCompileMsi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to link MSI.. + /// + internal static string FailedToLinkMsi { + get { + return ResourceManager.GetString("FailedToLinkMsi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to harvest package contents.. + /// + internal static string HeatFailedToHarvest { + get { + return ResourceManager.GetString("HeatFailedToHarvest", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string NoManifestInstallerVersion { + get { + return ResourceManager.GetString("NoManifestInstallerVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Relative package path exceeds the maximum length ({0}): {1}.. + /// + internal static string RelativePackagePathTooLong { + get { + return ResourceManager.GetString("RelativePackagePathTooLong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum version must be greater than or equal to the minimum version, or null. . + /// + internal static string SwixDependencyMaxVersionLessThanMinVersion { + get { + return ResourceManager.GetString("SwixDependencyMaxVersionLessThanMinVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A SWIX dependency must have a version boundary. The minimum and maximum versions cannot both be null.. + /// + internal static string SwixDependencyVersionRequired { + get { + return ResourceManager.GetString("SwixDependencyVersionRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find a template matching the provided key: {0}.. + /// + internal static string TemplateNotFound { + get { + return ResourceManager.GetString("TemplateNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not read the template resource: {0}.. + /// + internal static string TemplateResourceNotFound { + get { + return ResourceManager.GetString("TemplateResourceNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown workload kind: {0}.. + /// + internal static string UnknownWorkloadKind { + get { + return ResourceManager.GetString("UnknownWorkloadKind", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} does not support any Windows platforms and will be skipped. Supported platforms: {1}.. + /// + internal static string WorkloadDefinitionDoesNotSupportWindows { + get { + return ResourceManager.GetString("WorkloadDefinitionDoesNotSupportWindows", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to find the workload manifest. Neither '{0}' nor '{1}' exists.. + /// + internal static string WorkloadManifestNotFound { + get { + return ResourceManager.GetString("WorkloadManifestNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleting '{0}' because it is excluded.. + /// + internal static string WorkloadPackageDeleteExclusion { + get { + return ResourceManager.GetString("WorkloadPackageDeleteExclusion", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx new file mode 100644 index 00000000000..e68d9f88641 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Extracting package: {0}. + + + Unable to extract the manifest ID from the package ID: {0}. + + + Unable to extract the SDK version from the package ID: {0}. + + + Components cannot have a null description. Either provide a custom resource or add a description to the workload definition. + + + A SWIX component must have at least one dependency, Id: {0}. + + + Components cannot have a null title. Either provide a custom resource or add a description to the workload definition. + + + Failed to compile MSI. + + + Failed to link MSI. + + + Failed to harvest package contents. + + + 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. + + + Relative package path exceeds the maximum length ({0}): {1}. + + + The maximum version must be greater than or equal to the minimum version, or null. + + + A SWIX dependency must have a version boundary. The minimum and maximum versions cannot both be null. + + + Could not find a template matching the provided key: {0}. + + + Could not read the template resource: {0}. + + + Unknown workload kind: {0}. + + + {0} does not support any Windows platforms and will be skipped. Supported platforms: {1}. + + + Unable to find the workload manifest. Neither '{0}' nor '{1}' exists. + + + Deleting '{0}' because it is excluded. + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs new file mode 100644 index 00000000000..eafa23b41f9 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs @@ -0,0 +1,77 @@ +// 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; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix +{ + /// + /// Creates a SWIX project used to author a Visual Studio component package. + /// + internal class ComponentSwixProject : SwixProjectBase + { + private SwixComponent _component; + + protected override string ProjectFile + { + get; + } + + /// + protected override string ProjectSourceDirectory + { + get; + } + + public ComponentSwixProject(SwixComponent component, string baseIntermediateOutputPath, string baseOutputPath) : + base(component.Name, component.Version, baseIntermediateOutputPath, baseOutputPath) + { + _component = component; + ValidateRelativePackagePath($@"{component.Name},version={component.Version}\_package.json"); + + // Components must have 1 or more dependencies. + if (!component.HasDependencies) + { + throw new ArgumentException(string.Format(Strings.ComponentMustHaveAtLeastOneDependency, component.Name)); + } + + ProjectSourceDirectory = Path.Combine(SwixDirectory, $"{component.SdkFeatureBand}", + $"{component.Name}.{component.Version}"); + + ReplacementTokens[SwixTokens.__VS_COMPONENT_TITLE__] = component.Title; + ReplacementTokens[SwixTokens.__VS_COMPONENT_DESCRIPTION__] = component.Description; + ReplacementTokens[SwixTokens.__VS_COMPONENT_CATEGORY__] = component.Category; + ReplacementTokens[SwixTokens.__VS_IS_UI_GROUP__] = component.IsUiGroup ? "yes" : "no"; + } + + /// + public override string Create() + { + string swixProj = EmbeddedTemplates.Extract("component.swixproj", ProjectSourceDirectory, $"{Id}.{Version.ToString(2)}.swixproj"); + string componentSwr = EmbeddedTemplates.Extract("component.swr", ProjectSourceDirectory); + + ReplaceTokens(swixProj); + ReplaceTokens(EmbeddedTemplates.Extract("component.res.swr", ProjectSourceDirectory)); + ReplaceTokens(componentSwr); + + // SWIX is indentation sensitive. The dependencies should be written as + // + // vs.dependencies + // vs.dependency id= + // version= + using StreamWriter swrWriter = File.AppendText(componentSwr); + + foreach (SwixDependency dependency in _component.Dependencies) + { + swrWriter.WriteLine($" vs.dependency id={dependency.Id}"); + swrWriter.WriteLine($" version={dependency.GetVersionRange()}"); + swrWriter.WriteLine($" behaviors=IgnoreApplicabilityFailures"); + } + + return swixProj; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs new file mode 100644 index 00000000000..be5fdf37cfa --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix +{ + /// + /// Creates a SWIX project for an MSI package. + /// + public class MsiSwixProject : SwixProjectBase + { + /// + /// The target platform of the package. + /// + protected string Chip + { + get; + } + + /// + protected override string ProjectFile + { + get; + } + + /// + protected override string ProjectSourceDirectory + { + get; + } + + public MsiSwixProject(ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, + string visualStudioProductArchitecture = "neutral") : base(msi.GetMetadata(Metadata.SwixPackageId), new Version(msi.GetMetadata(Metadata.Version)), baseIntermediateOutputPath, baseOutputPath) + { + Chip = msi.GetMetadata(Metadata.Platform); + ProjectSourceDirectory = Path.Combine(SwixDirectory, Id, Chip); + + ValidateRelativePackagePath($@"{Id},version={Version},chip={Chip},productarch={visualStudioProductArchitecture}\{Path.GetFileName(msi.ItemSpec)}"); + + // The name of the .swixproj file is used to create the JSON manifest that will be merged into the .vsman file later. + // For drop publishing all the JSON manifests and payloads must reside in the same folder so we shorten the project names + // and use a hashed filename to avoid path too long errors. + string projectName = $"{Id}.{Version.ToString(3)}.{Chip}"; + ProjectFile = $"{Utils.GetHash(projectName, HashAlgorithmName.MD5)}.swixproj"; + + FileInfo fileInfo = new(msi.ItemSpec); + + ReplacementTokens[SwixTokens.__VS_PACKAGE_CHIP__] = Chip; + ReplacementTokens[SwixTokens.__VS_PACKAGE_INSTALL_SIZE_SYSTEM_DRIVE__] = $"{MsiUtils.GetInstallSize(msi.ItemSpec)}"; + ReplacementTokens[SwixTokens.__VS_PACKAGE_PRODUCT_ARCH__] = visualStudioProductArchitecture; + ReplacementTokens[SwixTokens.__VS_PAYLOAD_SIZE__] = $"{fileInfo.Length}"; + ReplacementTokens[SwixTokens.__VS_PAYLOAD_SOURCE__] = msi.GetMetadata(Metadata.FullPath); + } + + /// + public override string Create() + { + string swixProj = EmbeddedTemplates.Extract("msi.swixproj", ProjectSourceDirectory, ProjectFile); + + Utils.StringReplace(swixProj, ReplacementTokens, Encoding.UTF8); + Utils.StringReplace(EmbeddedTemplates.Extract("msi.swr", ProjectSourceDirectory), ReplacementTokens, Encoding.UTF8); + + return swixProj; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs new file mode 100644 index 00000000000..10cc3aff0f1 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs @@ -0,0 +1,214 @@ +// 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.Linq; +using Microsoft.Build.Framework; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix +{ + /// + /// Represents a component or component group in Visual Studio Installer. + /// + internal class SwixComponent + { + /// + /// RIDs supported on Windows. Only pack dependencies that contain these RIDs will be added to the component. + /// + private static readonly string[] s_SupportedRids = new string[] { "win7", "win10", "any", "win", "win-x64", "win-x86", "win-arm64" }; + + /// + /// Default version to assign to component dependencies. + /// + private static readonly Version s_v1 = new Version("1.0.0.0"); + + private List _dependencies = new(); + + /// + /// The component category. + /// + public string Category + { + get; + } = DefaultValues.ComponentCategory; + + /// + /// Gets the set of packages and components on which this component depends. + /// + public IEnumerable Dependencies => _dependencies; + + /// + /// The description of the component, displayed as a tooltip inside the UI. + /// + public string Description + { + get; + } + + /// + /// Gets whether this component has any dependencies. + /// + public bool HasDependencies => _dependencies.Count > 0; + + /// + /// When , this component represents a component group that is only visible as a top-level + /// dependency in the wokloads tab. Otherwise it is a visible component that becomes selectable in the individual components + /// tab. + /// + public bool IsUiGroup + { + get; + } + + /// + /// The component name (ID). + /// + public string Name + { + get; + } + + /// + /// The SDK feature band associated with this component. + /// + public ReleaseVersion SdkFeatureBand + { + get; + } + + /// + /// A set of items used to shorten the names and identifiers of setup packages. + /// + public ITaskItem[]? ShortNames + { + get; + } + + /// + /// The title of the component to display in the installer UI, e.g. the individual component tab. + /// + public string Title + { + get; + } + + /// + /// The version of the component. + /// + public Version Version + { + get; + } + + /// + /// Creates a new SWIX component. + /// + /// The SDK feature band associated with the component. + /// The component ID. + /// /// The component title as it appears next to checkboxes on the workload and individual component tabs. + /// The component description, displayed as a tooltip in the Visual Studio Installer UI. + /// The version of the component. + /// When , indicates that this component is a component group and + /// will be hidden on the individual components tab. + /// The category associated with the component. The value acts as a grouping mechanism on + /// the individual components tab. + /// A set of items used to shorten the names and identifiers of setup packages. + internal SwixComponent(ReleaseVersion sdkFeatureBand, string name, string title, string description, Version version, + bool isUiGroup, string category, ITaskItem[]? shortNames) + { + Name = name; + Title = title; + Description = description; + Version = version; + IsUiGroup = isUiGroup; + Category = category; + SdkFeatureBand = sdkFeatureBand; + ShortNames = shortNames; + } + + /// + /// Adds a depdency using the specified ID and versions. + /// + /// The SWIX ID of the dependency. + /// The minimum dependency version. + /// The maximum dependency version. + public void AddDependency(string id, Version? minVersion = null, Version? maxVersion = null) + { + _dependencies.Add(new SwixDependency(id, minVersion, maxVersion)); + } + + /// + /// Adds a dependency using the specified workload pack. + /// + /// The workload pack to add as a dependency. + public void AddDependency(WorkloadPack pack) + { + _dependencies.Add(new SwixDependency($"{pack.Id.ToString().Replace(ShortNames)}.{pack.Version}", new NuGetVersion(pack.Version).Version, maxVersion: null)); + } + + /// + /// Creates a SWIX component representing a workload definition. + /// + /// The SDK featureband associated with the workload manifest. + /// The workload definition to use for the component. + /// The workload manifest to which the workload belongs. + /// Additional resources that can be used to override component attributes such + /// as the title, description, and category. + /// A set of items used to shorten the names of setup packages. + /// A SWIX component. + public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefinition workload, WorkloadManifest manifest, + ITaskItem[]? componentResources = null, ITaskItem[]? shortNames = null) + { + ITaskItem? resourceItem = componentResources?.Where(r => string.Equals(r.ItemSpec, workload.Id)).FirstOrDefault(); + + // If no explicit version mapping exists for the workload, the major.minor.patch version of the manifest is used + // for the component version in Visual Studio Installer. + Version componentVersion = resourceItem != null && !string.IsNullOrWhiteSpace(resourceItem.GetMetadata(Metadata.Version)) ? + new Version(resourceItem.GetMetadata(Metadata.Version)) : + new Version((new ReleaseVersion(manifest.Version)).ToString(3)); + + // Since workloads only define a description, if no custom resources were provided, both the title and description of + // the SWIX component will default to the workload description. + SwixComponent component = new(sdkFeatureBand, Utils.ToSafeId(workload.Id), + resourceItem?.GetMetadata(Metadata.Title) ?? workload.Description ?? throw new Exception(Strings.ComponentTitleCannotBeNull), + resourceItem?.GetMetadata(Metadata.Description) ?? workload.Description ?? throw new Exception(Strings.ComponentDescriptionCannotBeNull), + componentVersion, workload.IsAbstract, + resourceItem?.GetMetadata(Metadata.Category) ?? DefaultValues.ComponentCategory, + shortNames); + + // If the workload extends other workloads, we add those as component dependencies before + // processing direct pack dependencies. + foreach (WorkloadId dependency in workload.Extends ?? Enumerable.Empty()) + { + component.AddDependency(Utils.ToSafeId(dependency), s_v1); + } + + // TODO: Check for missing packs + + foreach (WorkloadPackId packId in workload.Packs ?? Enumerable.Empty()) + { + // Check whether the pack dependency is aliased to non-Windows RIDs. If so, we can't add a dependency for the pack + // because we won't be able to create any installers. + if (manifest.Packs.TryGetValue(packId, out WorkloadPack? pack)) + { + if (pack.IsAlias && pack.AliasTo != null && !pack.AliasTo.Keys.Any(rid => s_SupportedRids.Contains(rid))) + { + continue; + } + + component.AddDependency(manifest.Packs[packId]); + } + } + + return component; + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixDependency.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixDependency.cs new file mode 100644 index 00000000000..f3087758c8e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixDependency.cs @@ -0,0 +1,90 @@ +// 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; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a Visual Studio setup dependency. + /// + public class SwixDependency + { + /// + /// The SWIX package ID of the dependency. + /// + public string Id + { + get; + } + + /// + /// The minimum dependency version. + /// + public Version? MinVersion + { + get; + } + + /// + /// The maxmimum dependency version. + /// + public Version? MaxVersion + { + get; + } + + /// + /// Creates a new dependency with an exact version. + /// + /// The SWIX package ID. The ID applies to packages, components, component groups, etc. + /// The exact version of the dependency. + public SwixDependency(string id, Version version) : this(id, version, version) + { + + } + + /// + /// Creates a dependency with a minimum and/or maximum version. + /// + /// The SWIX package ID. The ID applies to packages, components, component groups, etc. + /// The minimum required version, inclusive. May be if only an upper bound is required. + /// The maximum version, exclusive. May be if only a lower bound is required. + /// If equal to , an exact version requirement is created, e.g. [1.2.0]. + public SwixDependency(string id, Version? minVersion, Version? maxVersion) + { + if ((minVersion == null) && (maxVersion == null)) + { + throw new ArgumentException(Strings.SwixDependencyVersionRequired); + } + + if ((maxVersion != null) && (minVersion != null) && (maxVersion < minVersion)) + { + throw new ArgumentException(Strings.SwixDependencyMaxVersionLessThanMinVersion); + } + + Id = id; + MinVersion = minVersion; + MaxVersion = maxVersion; + } + + /// + /// Gets a string describing the version range of the dependency. + /// + /// + public string GetVersionRange() + { + if ((MinVersion != null) && (MinVersion == MaxVersion)) + { + return $"[{MinVersion}]"; + } + + + return $"[{MinVersion?.ToString()},{MaxVersion?.ToString()})"; + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs new file mode 100644 index 00000000000..a6f334cf16b --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix +{ + /// + /// A base class to create SWIX projects for Visual Studio setup packages. + /// + public abstract class SwixProjectBase : ProjectTemplateBase + { + /// + /// The maximum relative path length of a package. The length accounts for the Visual Studio package cache + /// created at install time. + /// + public const int MaxRelativePackagePath = 182; + + /// + /// The ID of the SWIX package in the Visual Studio setup catalog. The ID is used + /// to reference the package inside other setup packages such as components and component groups. + /// + public string Id + { + get; + set; + } + + /// + /// The version of the SWIX package. + /// + public Version Version + { + get; + set; + } + + /// + /// The root directory for SWIX projects. + /// + protected string SwixDirectory => Path.Combine(SourceDirectory, "swix"); + + /// + /// + /// + /// The SWIX package ID. + /// The package version. + public SwixProjectBase(string id, Version version, string baseIntermediateOutputPath, string baseOutputPath) : + base(baseIntermediateOutputPath, baseOutputPath) + { + Id = id; + Version = version; + + ReplacementTokens[SwixTokens.__VS_PACKAGE_NAME__] = Id; + ReplacementTokens[SwixTokens.__VS_PACKAGE_VERSION__] = $"{Version}"; + } + + /// + /// Replace all tokens in the specified file. + /// + /// The path of the file to update. + protected void ReplaceTokens(string path) + { + Utils.StringReplace(path, ReplacementTokens, Encoding.UTF8); + } + + /// + /// Validates that the length of the relative package path does not execeed the maximum limit allowed by Visual Studio. The length + /// accounts for the location of the Visual Studio installer package cache. + /// + /// + internal static void ValidateRelativePackagePath(string relativePackagePath) + { + _ = relativePackagePath ?? throw new ArgumentNullException(nameof(relativePackagePath)); + + // Visual Studio will verify this as part of its manifest validation logic during PR builds, but + // any error would require rebuilding workloads and effectively reset .NET builds. + if (relativePackagePath.Length > MaxRelativePackagePath) + { + throw new Exception(string.Format(Strings.RelativePackagePathTooLong, MaxRelativePackagePath, relativePackagePath)); + } + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixTokens.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixTokens.cs new file mode 100644 index 00000000000..af2a5229cbc --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixTokens.cs @@ -0,0 +1,23 @@ +// 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.Swix +{ + /// + /// Defines replacement token names used by SWIX project templates. + /// + public static class SwixTokens + { + public static readonly string __VS_COMPONENT_CATEGORY__ = nameof(__VS_COMPONENT_CATEGORY__); + public static readonly string __VS_COMPONENT_DESCRIPTION__ = nameof(__VS_COMPONENT_DESCRIPTION__); + public static readonly string __VS_COMPONENT_TITLE__ = nameof(__VS_COMPONENT_TITLE__); + public static readonly string __VS_IS_UI_GROUP__ = nameof(__VS_IS_UI_GROUP__); + public static readonly string __VS_PACKAGE_CHIP__ = nameof(__VS_PACKAGE_CHIP__); + public static readonly string __VS_PACKAGE_INSTALL_SIZE_SYSTEM_DRIVE__ = nameof(__VS_PACKAGE_INSTALL_SIZE_SYSTEM_DRIVE__); + public static readonly string __VS_PACKAGE_NAME__ = nameof(__VS_PACKAGE_NAME__); + public static readonly string __VS_PACKAGE_PRODUCT_ARCH__ = nameof(__VS_PACKAGE_PRODUCT_ARCH__); + public static readonly string __VS_PAYLOAD_SIZE__ = nameof(__VS_PAYLOAD_SIZE__); + public static readonly string __VS_PAYLOAD_SOURCE__ = nameof(__VS_PAYLOAD_SOURCE__); + public static readonly string __VS_PACKAGE_VERSION__ = nameof(__VS_PACKAGE_VERSION__); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SwixTemplate/msi.swr b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SwixTemplate/msi.swr index 5108da84125..79fcec8ed83 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SwixTemplate/msi.swr +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/SwixTemplate/msi.swr @@ -12,7 +12,7 @@ vs.installSize SharedDrive=0 vs.logFiles - vs.logFile pattern="dd_seutp*__VS_PACKAGE_NAME__*.log" + vs.logFile pattern="dd_setup*__VS_PACKAGE_NAME__*.log" vs.msiProperties vs.msiProperty name="MSIFASTINSTALL" value="7" diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/TemplatePackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/TemplatePackPackage.wix.cs new file mode 100644 index 00000000000..a6476ab4f7e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/TemplatePackPackage.wix.cs @@ -0,0 +1,27 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a template pack. + /// + internal class TemplatePackPackage : WorkloadPackPackage + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Copy; + + public TemplatePackPackage(WorkloadPack pack, string packagePath, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(pack, packagePath, platforms, destinationBaseDirectory, shortNames, log) + { } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsPackPackage.wix.cs new file mode 100644 index 00000000000..841f9811c88 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsPackPackage.wix.cs @@ -0,0 +1,27 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a tools pack. + /// + internal class ToolsPackPackage : WorkloadPackPackage + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; + + public ToolsPackPackage(WorkloadPack pack, string packagePath, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + base(pack, packagePath, platforms, destinationBaseDirectory, shortNames, log) + { } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs index b039644119e..ef9a89b67b9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs @@ -19,16 +19,15 @@ public class Utils /// The value to hash. /// The name of the to use. /// A string containing the hash result. - public static string GetHash(string value, string hashName) + public static string GetHash(string value, HashAlgorithmName hashName) { byte[] bytes = Encoding.UTF8.GetBytes(value); #pragma warning disable SYSLIB0045 // Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead. - HashAlgorithm ha = HashAlgorithm.Create(hashName); + HashAlgorithm algorithm = HashAlgorithm.Create(hashName.Name); #pragma warning restore SYSLIB0045 - byte[] hash = ha.ComputeHash(bytes); + StringBuilder sb = new(); - var sb = new StringBuilder(); - foreach (byte b in hash) + foreach (byte b in algorithm.ComputeHash(bytes)) { sb.Append(b.ToString("x2")); } @@ -36,18 +35,37 @@ public static string GetHash(string value, string hashName) return sb.ToString(); } - internal static string ToSafeId(string id) + /// + /// Updates a string containing a path and ensures that it contains a single directory separator at the end. + /// + /// The original path value. + /// The modified path. + internal static string EnsureTrailingSlash(string path) { - return id.Replace("-", ".").Replace(" ", ".").Replace("_", "."); + return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; } + /// + /// Generates a safe SWIX ID by replacing "-", " ", and "_" with ".". + /// + /// The identifier to convert to a safe identifier + /// The safe identifier. + internal static string ToSafeId(string id) => + id.Replace("-", ".").Replace(" ", ".").Replace("_", "."); + + /// + /// Replaces all the tokens in a file using the provided dictionary. The dictionary keys define the tokens and + /// their values the replacement strings. + /// + /// The file to modify. + /// A dictionary containing the replacement tokens and values. + /// The encoding to use when updating the file. internal static void StringReplace(string fileName, Dictionary tokenReplacements, Encoding encoding) { FileAttributes oldAttributes = File.GetAttributes(fileName); File.SetAttributes(fileName, oldAttributes & ~FileAttributes.ReadOnly); string content = File.ReadAllText(fileName); - string newContent = File.ReadAllText(fileName); foreach (string token in tokenReplacements.Keys) { @@ -91,7 +109,7 @@ public static string ConvertToRegexPattern(string wildcardPattern) } else { - return String.Concat(escapedPattern, "$"); + return string.Concat(escapedPattern, "$"); } } @@ -158,6 +176,10 @@ public static Guid CreateUuid(Guid namespaceUuid, string name) return new Guid(hash); } + /// + /// Deletes the specified directory and all subdirectories if it exists. + /// + /// The directory to delete. internal static void DeleteDirectory(string path) { if (Directory.Exists(path)) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioComponent.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioComponent.cs deleted file mode 100644 index 97f9d1ac855..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioComponent.cs +++ /dev/null @@ -1,297 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.NET.Sdk.WorkloadManifestReader; -using NuGet.Versioning; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Represents a Visual Studio component or component group. - /// - public class VisualStudioComponent - { - private static readonly string[] s_SupportedRids = new string[] { "win7", "win10", "any", "win", "win-x64", "win-x86", "win-arm64" }; - - /// - /// The component category. - /// - public string Category - { - get; - } = ".NET"; - - /// - /// The description of the component, displayed as a tooltip inside the UI. - /// - public string Description - { - get; - } - - /// - /// Gets whether this component has any dependencies. - /// - public bool HasDependencies => Dependencies.Count > 0; - - /// - /// When "no", the component is visible in both the workloads and individual components tab. - /// When "yes", the component is only visible in the workloads tab. - /// - public string IsUiGroup - { - get; - } = "no"; - - /// - /// The component name (ID). - /// - public string Name - { - get; - } - - /// - /// An item group containing information to shorten the names of packages. - /// - public ITaskItem[] ShortNames - { - get; - set; - } - - /// - /// The title of the component to display in the installer UI, e.g. the individual component tab. - /// - public string Title - { - get; - } - - /// - /// The version of the component. - /// - public Version Version - { - get; - } - - private ICollection Dependencies = new List(); - - public VisualStudioComponent(string name, string description, string title, Version version, string isUiGroup, ITaskItem[] shortNames, - string category) - { - Name = name; - Description = description; - Title = title; - Version = version; - ShortNames = shortNames; - Category = category; - IsUiGroup = isUiGroup; - } - - /// - /// Add a component dependency using the provided name and version. - /// - /// The name (ID) of the dependency. - /// The version of the dependency. - public void AddDependency(string name, Version exactVersion) - { - AddDependency(new VisualStudioDependency(name, exactVersion, exactVersion)); - } - - /// - /// Add a component dependency using the provided name and version. - /// - /// The name (ID) of the dependency. - /// The version of the dependency. - public void AddDependency(string name, Version minVersion, Version maxVersion) - { - AddDependency(new VisualStudioDependency(name, minVersion, maxVersion)); - } - - /// - /// Add a component dependency using the specified . - /// - /// The dependency to add to this component. - public void AddDependency(VisualStudioDependency dependency) - { - Dependencies.Add(dependency); - } - - /// - /// Add a component dependency using the specified item. - /// - /// The dependency to add to this component. - public void AddDependency(ITaskItem dependency) - { - AddDependency(new VisualStudioDependency(dependency.ItemSpec.Replace(ShortNames), new Version(dependency.GetMetadata(Metadata.Version)))); - } - - /// - /// Add a dependency using the specified workload pack. - /// - /// The dependency to add to this component. - public void AddDependency(WorkloadPack pack) - { - AddDependency($"{pack.Id.ToString().Replace(ShortNames)}.{pack.Version}", new NuGetVersion(pack.Version).Version, maxVersion: null); - } - - public IEnumerable GetAliasedDependencies(WorkloadPack pack) - { - foreach (var rid in pack.AliasTo.Keys) - { - switch (rid) - { - case "win-x86": - case "win-x64": - yield return new VisualStudioDependency($"{pack.AliasTo[rid].ToString().Replace(ShortNames)}.{pack.Version}", new NuGetVersion(pack.Version).Version); - break; - default: - break; - } - } - } - - /// - /// Generate a SWIX project for the component in the specified folder. - /// - /// The path of the SWIX project to generate. - /// An item describing the generated SWIX project. - public TaskItem Generate(string projectPath) - { - string componentSwr = EmbeddedTemplates.Extract("component.swr", projectPath); - string componentResSwr = EmbeddedTemplates.Extract("component.res.swr", projectPath); - string componentSwixProj = EmbeddedTemplates.Extract("component.swixproj", projectPath, $"{Name}.{Version.ToString(2)}.swixproj"); - - Dictionary replacementTokens = GetReplacementTokens(); - Utils.StringReplace(componentSwr, replacementTokens, Encoding.UTF8); - Utils.StringReplace(componentResSwr, replacementTokens, Encoding.UTF8); - - using StreamWriter swrWriter = File.AppendText(componentSwr); - - foreach (VisualStudioDependency dependency in Dependencies) - { - // SWIX is indentation sensitive. The dependencies should be written as - // - // vs.dependencies - // vs.dependency id= - // version=[1.2.3.4] - - swrWriter.WriteLine($" vs.dependency id={dependency.Id}"); - swrWriter.WriteLine($" version={dependency.GetVersion()}"); - swrWriter.WriteLine($" behaviors=IgnoreApplicabilityFailures"); - } - - return new TaskItem(componentSwixProj); - } - - private Dictionary GetReplacementTokens() - { - return new Dictionary() - { - {"__VS_PACKAGE_NAME__", Name }, - {"__VS_PACKAGE_VERSION__", Version.ToString() }, - {"__VS_COMPONENT_TITLE__", Title }, - {"__VS_COMPONENT_DESCRIPTION__", Description }, - {"__VS_COMPONENT_CATEGORY__", Category ?? ".NET" }, - {"__VS_IS_UI_GROUP__", IsUiGroup ?? "no" } - }; - } - - /// - /// Creates a using a workload definition. - /// - /// - /// - /// - /// - /// - /// - /// - public static VisualStudioComponent Create(TaskLoggingHelper log, WorkloadManifest manifest, WorkloadDefinition workload, ITaskItem[] componentVersions, - ITaskItem[] shortNames, ITaskItem[] componentResources, ITaskItem[] missingPacks) - { - log?.LogMessage("Creating Visual Studio component"); - string workloadId = $"{workload.Id}"; - - // If there's an explicit version mapping we use that, otherwise we fall back to the manifest version - // and normalize it since it can have semantic information and Visual Studio components do not support that. - ITaskItem versionItem = componentVersions?.Where(v => string.Equals(v.ItemSpec, workloadId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - Version version = (versionItem != null) && !string.IsNullOrWhiteSpace(versionItem.GetMetadata(Metadata.Version)) - ? new Version(versionItem.GetMetadata(Metadata.Version)) - : (new NuGetVersion(manifest.Version)).Version; - - ITaskItem resourceItem = componentResources?.Where( - r => string.Equals(r.ItemSpec, workloadId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - - // Workload definitions do not have separate title/description fields so the only option - // is to default to the workload description for both. - string title = resourceItem?.GetMetadata(Metadata.Title) ?? workload.Description; - string description = resourceItem?.GetMetadata(Metadata.Description) ?? workload.Description; - string category = resourceItem?.GetMetadata(Metadata.Category) ?? ".NET"; - string isUiGroup = workload.IsAbstract ? "yes" : "no"; - - VisualStudioComponent component = new(Utils.ToSafeId(workloadId), description, - title, version, isUiGroup, shortNames, category); - - IEnumerable missingPackIds = missingPacks.Select(p => p.ItemSpec); - log?.LogMessage(MessageImportance.Low, $"Missing packs: {string.Join(", ", missingPackIds)}"); - - // If the workload extends other workloads, we add those as component dependencies before - // processing direct pack dependencies - if (workload.Extends?.Count() > 0) - { - foreach (WorkloadId dependency in workload.Extends) - { - // Component dependencies, aka. workload extensions only have minimum version dependencies. - component.AddDependency($"{Utils.ToSafeId(dependency.ToString())}", new Version("1.0.0.0"), maxVersion: null); - } - } - - if (workload.Packs != null) - { - // Visual Studio is case-insensitive. - IEnumerable packIds = workload.Packs.Where(p => !missingPackIds.Contains($"{p}", StringComparer.OrdinalIgnoreCase)); - log?.LogMessage(MessageImportance.Low, $"Packs: {string.Join(", ", packIds.Select(p => $"{p}"))}"); - - foreach (WorkloadPackId packId in packIds) - { - // Check whether the pack dependency is aliased to non-Windows RIDs. If so, we can't add a dependency for the pack - // because we won't be able to create installers. - if (manifest.Packs.TryGetValue(packId, out WorkloadPack pack)) - { - if (pack.IsAlias && !pack.AliasTo.Keys.Any(rid => s_SupportedRids.Contains(rid))) - { - log?.LogMessage($"Workload {workload.Id} supports Windows, but none of its aliased packs do. Only the following pack aliases were found: {String.Join("", "", pack.AliasTo.Keys)}."); - continue; - } - else - { - log?.LogMessage(MessageImportance.Low, $"Adding component dependency for {packId} "); - component.AddDependency(manifest.Packs[packId]); - } - } - - //if (manifest.Packs.ContainsKey(packId)) - //{ - // component.AddDependency(manifest.Packs[packId]); - //} - //else - //{ - // log?.LogMessage(MessageImportance.High, $"Missing Visual Studio component dependency, packId: {packId}."); - //} - } - } - - return component; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioDependency.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioDependency.cs deleted file mode 100644 index 0f26d13e0c0..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioDependency.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Represents a Visual Studio setup dependency. - /// - public class VisualStudioDependency - { - /// - /// The identifier of the dependent (package, component, etc.). - /// - public string Id - { - get; - } - - public Version MinVersion - { - get; - } - - public Version MaxVersion - { - get; - } - - /// - /// Creates a dependency with an exact version. - /// - /// - /// - public VisualStudioDependency(string id, Version version) : this(id, version, version) - { - - } - - /// - /// Creates a dependency with a minimum and maximum versions. - /// - /// The Visual Studio package ID. The ID applies to packages, components, component groups, etc. - /// The minimum required version, inclusive. - /// The maximum version, exclusive. May be if there is only a minimum requirement. If - /// equal to , an exact version requirement is created, e.g. [1.2.0]. - public VisualStudioDependency(string id, Version minVersion, Version maxVersion) - { - Id = id; - MinVersion = minVersion; - MaxVersion = maxVersion; - } - - public string GetVersion() - { - if ((MaxVersion != null) && (MinVersion == MaxVersion)) - { - return $"[{MinVersion}]"; - } - - if (MaxVersion == null) - { - return $"[{MinVersion},)"; - } - - return $"[{MinVersion},{MaxVersion})"; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CommandLineBuilderExtensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CommandLineBuilderExtensions.cs similarity index 96% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/CommandLineBuilderExtensions.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CommandLineBuilderExtensions.cs index 10decc3d87c..b31b1edce53 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CommandLineBuilderExtensions.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CommandLineBuilderExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.Build.Utilities; -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { /// /// extension methods. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs new file mode 100644 index 00000000000..b3425c3a30b --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// A tool task used to invoke the WiX compiler (candle.exe). + /// + public class CompilerToolTask : WixToolTaskBase + { + private List _sourceFiles = new(); + + /// + /// The default architecture used for packages, components, etc. + /// + public string Architecture + { + get; + set; + } = "x86"; + + /// + /// The directory where the compiler output will be generated. + /// + public string OutputPath + { + get; + set; + } + + /// + /// The name of the WiX compiler executable. + /// + protected override string ToolName => "candle.exe"; + + /// + /// Creates a new instance that can be used to create an MSI. + /// + /// + /// + /// + /// + public CompilerToolTask(IBuildEngine engine, string wixToolsetPath, string outputPath, string architecture) : base(engine, wixToolsetPath) + { + OutputPath = outputPath; + Architecture = architecture; + } + + /// + /// Adds one or more source file to compile. + /// + /// The set of source files to compile. + public void AddSourceFiles(params string[] sourceFiles) + { + foreach (string sourceFile in sourceFiles) + { + _sourceFiles.Add(sourceFile); + } + } + + /// + protected override string GenerateCommandLineCommands() + { + CommandLineBuilder.AppendSwitchIfNotNull("-out ", OutputPath); + // No trailing space, preprocessor definitions are passed as -d= + CommandLineBuilder.AppendArrayIfNotNull("-d", PreprocessorDefinitions.ToArray()); + CommandLineBuilder.AppendArrayIfNotNull("-ext ", Extensions.ToArray()); + CommandLineBuilder.AppendSwitchIfNotNull("-arch ", Architecture); + CommandLineBuilder.AppendFileNamesIfNotNull(_sourceFiles.ToArray(), " "); + return CommandLineBuilder.ToString(); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GuidOptions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/GuidOptions.cs similarity index 91% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/GuidOptions.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/GuidOptions.cs index 8ee1b24e1b8..bd027a080a9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GuidOptions.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/GuidOptions.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { /// /// An enumeration that defines the GUID generation options used by the Heat command. @@ -12,6 +12,7 @@ public enum GuidOptions /// Generate GUIDs now, during harvesting (-gg). /// GenerateNow, + /// /// Autogenerate GUIDs at compile time (-ag). /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/HarvestToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs similarity index 85% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/HarvestToolTask.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs index 8f6c9a2797a..e51f3403766 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/HarvestToolTask.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs @@ -5,13 +5,18 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { /// - /// A tool task to invoke the WiX harvesting tool (Heat.exe). + /// A tool task to invoke the WiX harvesting tool (heat.exe). /// - public class HarvestToolTask : WixToolTask + public class HarvesterToolTask : WixToolTaskBase { + /// + /// The default ID of the ComponentGroup generated by Heat. + /// + internal static readonly string DefaultPackageContentsComponentGroupName = "CG_PackageContents"; + private static readonly Dictionary s_SuppressionArguments = new() { { HeatSuppressions.SuppressComElements, "-scom" }, @@ -29,7 +34,7 @@ public string ComponentGroupName { get; set; - } + } = DefaultPackageContentsComponentGroupName; /// /// Gets or sets the name of the directory reference pointing to root directories. The name cannot contain any spaces. @@ -58,6 +63,12 @@ public string OutputFile set; } + public string Platform + { + get; + set; + } + /// /// The source directory to harvest. /// @@ -76,6 +87,9 @@ public HeatSuppressions Suppressions set; } = HeatSuppressions.SuppressRegistryHarvesting | HeatSuppressions.SuppressRootDirectory; + /// + /// The name of the WiX harvest too. + /// protected override string ToolName => "heat.exe"; /// @@ -84,7 +98,7 @@ public HeatSuppressions Suppressions /// The fully qualified path to the WiX toolset. /// The path of the directory to harvest. /// The fully qualified path of the output file to generate. - public HarvestToolTask(IBuildEngine engine, string wixToolsetPath) : base(engine, wixToolsetPath) + public HarvesterToolTask(IBuildEngine engine, string wixToolsetPath) : base(engine, wixToolsetPath) { } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/HeatSuppressions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HeatSuppressions.cs similarity index 95% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/HeatSuppressions.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HeatSuppressions.cs index f417ccb4991..7581440b9c8 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/HeatSuppressions.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HeatSuppressions.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { /// /// Flags corresponding to Heat commandline suppressions. @@ -15,22 +15,27 @@ public enum HeatSuppressions /// Suppress COM elements (-scom). /// SuppressComElements = 0x0001, + /// /// Suppress fragments (-sfrag). /// SuppressFragments = 0x0002, + /// /// Suppress harvesting the root directory as an element (-srd). /// SuppressRootDirectory = 0x0004, + /// /// Suppress registry harvesting (-sreg). /// SuppressRegistryHarvesting = 0x0008, + /// /// Suppress unique identifiers for files, components, and directories (-suid). /// SuppressUuid = 0x0010, + /// /// Suppress VB6 COM elements (-svb6). /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/LinkToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/LinkerToolTask.cs similarity index 51% rename from src/Microsoft.DotNet.Build.Tasks.Workloads/src/LinkToolTask.cs rename to src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/LinkerToolTask.cs index 2409736f603..5f6ddf2e20d 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/LinkToolTask.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/LinkerToolTask.cs @@ -6,32 +6,34 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Microsoft.DotNet.Build.Tasks.Workloads +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { - public class LinkToolTask : WixToolTask + /// + /// A tool task to invoke the WiX linker (light.exe). + /// + public class LinkerToolTask : WixToolTaskBase { - protected override string ToolName => "light.exe"; - + /// + /// Adds file version information to the MsiAssemblyName table. + /// public bool AddFileVersion { get; set; - } = true; + } = true; /// - /// A collection of compiler extension assemblies to use. + /// The name of the output file to generate. /// - public ICollection Extensions - { - get; - } = new List(); - public string OutputFile { get; set; } + /// + /// The source files (.wixobj) used to link the executable. + /// public IEnumerable SourceFiles { get; @@ -39,7 +41,7 @@ public IEnumerable SourceFiles } /// - /// Semicolon sepearate list of ICEs to suppress. + /// Semicolon sepearated list of internal consistency evaluators (ICEs) to suppress. /// public string SuppressIces { @@ -47,11 +49,28 @@ public string SuppressIces set; } - public LinkToolTask(IBuildEngine engine, string wixToolsetPath) : base(engine, wixToolsetPath) + /// + /// The name of the WiX linker. + /// + protected override string ToolName => "light.exe"; + + /// + /// Creates a new + /// + /// + /// + public LinkerToolTask(IBuildEngine engine, string wixToolsetPath) : base(engine, wixToolsetPath) { } + protected override bool HandleTaskExecutionErrors() + { + Log?.LogMessage(MessageImportance.High, $"Light exited with: {ExitCode}, HasLoggedErrors: {HasLoggedErrors}"); + + return base.HandleTaskExecutionErrors(); + } + protected override string GenerateCommandLineCommands() { CommandLineBuilder.AppendSwitchIfNotNull("-o ", OutputFile); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs new file mode 100644 index 00000000000..19b777c939b --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// Defines WiX preprocessor variable names. + /// + public static class PreprocessorDefinitionNames + { + public static readonly string DependencyProviderKeyName = nameof(DependencyProviderKeyName); + public static readonly string EulaRtf = nameof(EulaRtf); + public static readonly string InstallDir = nameof(InstallDir); + public static readonly string ManifestId = nameof(ManifestId); + public static readonly string Manufacturer = nameof(Manufacturer); + public static readonly string PackKind = nameof(PackKind); + public static readonly string PackageId = nameof(PackageId); + public static readonly string PackageVersion = nameof(PackageVersion); + public static readonly string Platform = nameof(Platform); + public static readonly string ProductCode = nameof(ProductCode); + public static readonly string ProductName = nameof(ProductName); + public static readonly string ProductVersion = nameof(ProductVersion); + public static readonly string SdkFeatureBandVersion = nameof(SdkFeatureBandVersion); + public static readonly string SourceDir = nameof(SourceDir); + public static readonly string UpgradeCode = nameof(UpgradeCode); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixExtensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixExtensions.cs new file mode 100644 index 00000000000..033daf2d8b8 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// Defines the names of well-known WiX extensions that can be used by different tools. + /// + public static class WixExtensions + { + /// + /// Provides custom actions and tables to support dependency provider keys used + /// to manage MSI reference counts. + /// + public static readonly string WixDependencyExtension = nameof(WixDependencyExtension); + + /// + /// Provides custom UI functionality such as different dialog sets for MSIs. + /// + public static readonly string WixUIExtension = nameof(WixUIExtension); + + /// + /// Provides various custom actions and compiler extensions for MSIs and bundles. + /// + public static readonly string WixUtilExtension = nameof(WixUtilExtension); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs new file mode 100644 index 00000000000..b358f519d09 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs @@ -0,0 +1,80 @@ +// 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; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// Serves as a base class for implementing a to invoke a WiX command. + /// + public abstract class WixToolTaskBase : ToolTask + { + private HashSet _extensions = new(); + private List _preprocessorDefinitions = new(); + + /// + /// Provides utility methods for constructing a commandline. + /// + protected CommandLineBuilder CommandLineBuilder + { + get; + } = new CommandLineBuilder(); + + /// + /// Gets the collection of extensions to pass to the underlying tool task. + /// + public IEnumerable Extensions => _extensions; + + /// + /// Gets the collection of preprocessor definitions. Each element represents a single definition. + /// For example, "SomeVar=Hello world" defines a preprocessor variable named SomeVar set to "Hello world". The + /// value of the variable will automatically be quoted when passed to the underlying tool. + /// + public IEnumerable PreprocessorDefinitions => _preprocessorDefinitions; + + /// + protected override MessageImportance StandardOutputLoggingImportance => MessageImportance.High; + + /// + /// + /// + /// + /// The path where the WiX toolset is located. + /// + protected WixToolTaskBase(IBuildEngine engine, string wixToolsetPath) + { + BuildEngine = engine ?? throw new ArgumentNullException(nameof(engine)); + ToolPath = wixToolsetPath; + } + + /// + /// Adds the specified extension to the tool task. + /// + /// The name of the WiX extension. See for a list of well known extensions. + public void AddExtension(string name) => + _extensions.Add(name); + + /// + /// Removes the specified extension from the tool task. + /// + /// The name of the WiX extension. See for a list of well known extensions. + public void RemoveExtension(string name) => + _extensions.Remove(name); + + + /// + /// Adds a new preprocessor definition. + /// + /// The name of the preprocessor variable. + /// The value of the preprocessor variable. + public void AddPreprocessorDefinition(string name, string value) => + _preprocessorDefinitions.Add($@"{name}={value}"); + + /// + protected override string GenerateFullPathToTool() => ToolPath; + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WixToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WixToolTask.cs deleted file mode 100644 index 7cd520a0b67..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WixToolTask.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.DotNet.Build.Tasks.Workloads -{ - /// - /// Abstract to invoke a WiX command. - /// - public abstract class WixToolTask : ToolTask - { - protected CommandLineBuilder CommandLineBuilder - { - get; - } = new CommandLineBuilder(); - - public string Platform - { - get; - set; - } - - /// - /// Gets the collection of preprocessor definitions. Each element represents a single definition. - /// For example, -dSomeVar="Hello world" defines a preprocessor variable named SomeVar set to "Hello world". - /// - public ICollection PreprocessorDefinitions - { - get; - } = new List(); - - protected override string ToolName => throw new NotImplementedException(); - - protected override MessageImportance StandardOutputLoggingImportance => MessageImportance.High; - - protected WixToolTask(IBuildEngine engine, string wixToolsetPath) - { - BuildEngine = engine ?? throw new ArgumentNullException(nameof(engine)); - Utils.CheckNullOrEmpty(nameof(wixToolsetPath), wixToolsetPath); - ToolPath = wixToolsetPath; - } - - protected override string GenerateFullPathToTool() - { - return ToolPath; - } - } -} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs new file mode 100644 index 00000000000..7b41b35ab5e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs @@ -0,0 +1,175 @@ +// 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.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a NuGet package containing a workload manifest. + /// + internal class WorkloadManifestPackage : WorkloadPackageBase + { + /// + public override PackageExtractionMethod ExtractionMethod => PackageExtractionMethod.Unzip; + + /// + /// Special separator value used in workload manifest package IDs. + /// + private const string ManifestSeparator = ".Manifest-"; + + /// + /// The filename and extension of the workload manifest file. + /// + private const string ManifestFileName = "WorkloadManifest.json"; + + /// + /// The workload manfiest ID. + /// + public string ManifestId + { + get; + } + + /// + public override Version MsiVersion + { + get; + } + + /// + /// The SDK feature band version associated with this manifest package. + /// + public ReleaseVersion SdkFeatureBand + { + get; + } + + /// + /// Creates a new instance of a . + /// + /// A task item for the workload manifest NuGet package. + /// The root directory where packages will be extracted. + /// The general MSI version to use when the package does not contain metadata for the installer version. + /// A set of items used to shorten the names and identifiers of setup packages. + /// A class containing task logging methods. + /// + public WorkloadManifestPackage(ITaskItem package, string destinationBaseDirectory, Version msiVersion, + ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : + 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 versioing 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)); + } + + SdkFeatureBand = GetSdkFeatureBandVersion(GetSdkVersion(Id)); + ManifestId = GetManifestId(Id); + } + + /// + /// Gets the path of the workload manifest file. + /// + /// The path of the workload manifest file + /// + public string GetManifestFile() + { + if (!HasBeenExtracted) + { + Extract(); + } + + string primaryManifest = Path.Combine(DestinationDirectory, "data", ManifestFileName); + string secondaryManifest = Path.Combine(DestinationDirectory, ManifestFileName); + + // Check the data directory first, otherwise fall back to the older format where manifests + // were in the root of the package. + return File.Exists(primaryManifest) ? primaryManifest : + File.Exists(secondaryManifest) ? secondaryManifest : + throw new FileNotFoundException(string.Format(Strings.WorkloadManifestNotFound, primaryManifest, secondaryManifest)); + } + + /// + /// Creates a instance using the parsed contents of the workload manifest file. + /// + /// The parsed workload manifest. + public WorkloadManifest GetManifest() + { + string workloadManifestFile = GetManifestFile(); + + return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(workloadManifestFile), File.OpenRead(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. + /// + /// The package ID from which to extract the SDK version. + /// 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)); + + /// + /// Extracts the manifest ID from the package ID. + /// + /// The package ID from which to extract the manifest ID. + /// The manifest ID. + /// + internal static string GetManifestId(string packageId) => + !string.IsNullOrWhiteSpace(packageId) && packageId.IndexOf(ManifestSeparator) > -1 ? + packageId.Substring(0, packageId.IndexOf(ManifestSeparator)) : + throw new FormatException(string.Format(Strings.CannotExtractManifestIdFromPackageId, packageId)); + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs new file mode 100644 index 00000000000..2b09a70914f --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs @@ -0,0 +1,112 @@ +// 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.Utilities; +using Microsoft.Build.Framework; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Represents a NuGet package for a workload pack. + /// + internal abstract class WorkloadPackPackage : WorkloadPackageBase + { + private readonly WorkloadPack _pack; + + public WorkloadPackKind Kind => _pack.Kind; + + /// + public override Version MsiVersion + { + get; + } + + /// + /// An array of all the supported installer target platforms for this package. + /// + public string[] Platforms + { + get; + } + + public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platforms, string destinationBaseDirectory, + ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : base(packagePath, destinationBaseDirectory, shortNames, log) + { + _pack = pack; + Platforms = platforms; + MsiVersion = Version; + } + + /// + /// Gets all the packages associated with a specific workload pack for all supported platforms. + /// + /// + /// An enumerable of tuples. Each tuple contains the full path of the NuGet package and support platforms. + internal static IEnumerable<(string, string[])> GetSourcePackages(string packageSource, WorkloadPack pack) + { + if (pack.IsAlias && pack.AliasTo != null) + { + foreach (string rid in pack.AliasTo.Keys) + { + string sourcePackage = Path.Combine(packageSource, $"{pack.AliasTo[rid]}.{pack.Version}.nupkg"); + + switch (rid) + { + case "win7": + case "win10": + case "win": + case "any": + yield return (sourcePackage, CreateVisualStudioWorkload.SupportedPlatforms); + break; + case "win-x64": + yield return (sourcePackage, new[] { "x64" }); + break; + case "win-x86": + yield return (sourcePackage, new[] { "x86" }); + break; + case "win-arm64": + yield return (sourcePackage, new[] { "arm64" }); + break; + default: + // Unsupported RID. + continue; + } + } + } + else + { + // For non-RID specific packs we'll produce MSIs for each supported platform. + yield return (Path.Combine(packageSource, $"{pack.Id}.{pack.Version}.nupkg"), CreateVisualStudioWorkload.SupportedPlatforms); + } + } + + /// + /// Creates a workload pack package from the provided NuGet package and workload pack. + /// + /// The workload pack determines the type of package to create. + /// The NuGet package to use. + /// The platforms that can be targeted by the package. + /// + /// A new . + /// + internal static WorkloadPackPackage Create(WorkloadPack pack, string sourcePackage, string[] platforms, + string destinationBaseDirectory, ITaskItem[]? shortNames, TaskLoggingHelper? log) => + pack.Kind switch + { + WorkloadPackKind.Sdk => new SdkPackPackage(pack, sourcePackage, platforms, destinationBaseDirectory, shortNames, log), + WorkloadPackKind.Framework => new FrameworkPackPackage(pack, sourcePackage, platforms, destinationBaseDirectory, shortNames, log), + WorkloadPackKind.Library => new LibraryPackPackage(pack, sourcePackage, platforms, destinationBaseDirectory, shortNames, log), + WorkloadPackKind.Template => new TemplatePackPackage(pack, sourcePackage, platforms, destinationBaseDirectory, shortNames, log), + WorkloadPackKind.Tool => new ToolsPackPackage(pack, sourcePackage, platforms, destinationBaseDirectory, shortNames, log), + _ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, pack.Kind)) + }; + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs new file mode 100644 index 00000000000..55005cd3815 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs @@ -0,0 +1,251 @@ +// 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.IO.Compression; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Serves as a base class for implementing different types of workload packages. The class captures some common + /// elements related to the underlying NuGet package. + /// + public abstract class WorkloadPackageBase + { + /// + /// The package authors. + /// + public string Authors + { + get; + } + + public string Copyright + { + get; + } + + public string Description + { + get; + } + + /// + /// Determines on the contents of the package is managed. + /// + public abstract PackageExtractionMethod ExtractionMethod + { + get; + } + + /// + /// The NuGet package identifier. + /// + public string Id => Identity.Id; + + /// + /// The identity of the NuGet package. + /// + public PackageIdentity Identity + { + get; + } + + /// + /// Gets whether the package has been extracted. + /// + public bool HasBeenExtracted + { + get; + private set; + } + + public LicenseMetadata LicenseData + { + get; + } + + public string LicenseUrl + { + get; + } + + /// + /// Gets the version to use for the generated MSI's ProductVersion property. + /// + public abstract Version MsiVersion + { + get; + } + + public string PackagePath + { + get; + } + + public string PackageFileName + { + get; + } + + public string ShortName + { + get; + } + + /// + /// A string containing the major, minor and patch version of the package. + /// + public string ProductVersion => $"{PackageVersion.Major}.{PackageVersion.Minor}.{PackageVersion.Patch}"; + + public string Title + { + get; + } + + public string ProjectUrl + { + get; + } + + /// + /// The version of the NuGet package. + /// + public NuGetVersion PackageVersion => Identity.Version; + + public ITaskItem[]? ShortNames + { + get; + } + + public string SwixPackageId + { + get; + } + + /// + /// Gets an instance of a class containing task logging methods. + /// + protected TaskLoggingHelper? Log + { + get; + } + + /// + /// A containing the major, minor, and patch version of the underlying NuGet package. + /// + public Version Version => Identity.Version.Version; + + /// + /// The destination directory where the package will be extracted. + /// + public string DestinationDirectory + { + get; + } + + /// + /// Creates a new instance of a class. + /// + /// The path of the NuGet package. + /// The root directory where packages will be extracted. + /// A set of items used to shorten the names and identifiers of setup packages. + /// A class containing task logging methods. + public WorkloadPackageBase(string packagePath, string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) + { + // Very important: If the underlying stream isn't closed, it will cause + // sharing violations when the package content is being extracted later. + using FileStream fs = new(packagePath, FileMode.Open); + using PackageArchiveReader reader = new(fs); + NuspecReader nuspec = reader.NuspecReader; + + Authors = nuspec.GetAuthors(); + Copyright = nuspec.GetCopyright(); + Description = nuspec.GetDescription(); + Identity = nuspec.GetIdentity(); + LicenseData = nuspec.GetLicenseMetadata(); + LicenseUrl = nuspec.GetLicenseUrl(); + ProjectUrl = nuspec.GetProjectUrl(); + Title = nuspec.GetTitle(); + + PackagePath = packagePath; + DestinationDirectory = Path.Combine(destinationBaseDirectory, $"{Identity}"); + ShortNames = shortNames; + + PackageFileName = Path.GetFileNameWithoutExtension(packagePath); + ShortName = PackageFileName.Replace(shortNames); + SwixPackageId = Id.Replace(shortNames); + Log = log; + } + + /// + /// Extracts the contents of the package based on + /// + public void Extract() + { + Extract(Enumerable.Empty()); + } + + /// + /// Extract the contents of the package and optionally delete files that match + /// the set of exclusions. + /// + /// A set of regular expression patterns used to determine if a + /// file should be excluded. Excluded files will be deleted after the package has been extracted. + public virtual void Extract(IEnumerable exclusionPatterns) + { + if (HasBeenExtracted) + { + return; + } + + Utils.DeleteDirectory(DestinationDirectory); + Directory.CreateDirectory(DestinationDirectory); + + if (ExtractionMethod == PackageExtractionMethod.Copy) + { + File.Copy(PackagePath, Path.Combine(DestinationDirectory, Path.GetFileName(PackagePath)), overwrite: true); + HasBeenExtracted = true; + } + else if (ExtractionMethod == PackageExtractionMethod.Unzip) + { + ZipFile.ExtractToDirectory(PackagePath, DestinationDirectory); + + // Remove unnecessary files and directories that we never want to ship. These are always present in a NuGet package. + Utils.DeleteDirectory(Path.Combine(DestinationDirectory, "_rels")); + Utils.DeleteDirectory(Path.Combine(DestinationDirectory, "package")); + + Utils.DeleteFile(Path.Combine(DestinationDirectory, ".signature.p7s")); + Utils.DeleteFile(Path.Combine(DestinationDirectory, "[Content_Types].xml")); + Utils.DeleteFile(Path.Combine(DestinationDirectory, $"{Id}.nuspec")); + + if (exclusionPatterns.Any()) + { + foreach (string file in Directory.EnumerateFiles(DestinationDirectory, "*.*", SearchOption.AllDirectories)) + { + if (exclusionPatterns.Any(pattern => Regex.IsMatch(file, pattern))) + { + Log?.LogMessage(MessageImportance.Low, string.Format(Strings.WorkloadPackageDeleteExclusion, file)); + File.Delete(file); + } + } + } + + HasBeenExtracted = true; + } + } + } +} + +#nullable disable From 5bcb29bb6bc97c757975b615e65b773b2b8acf04 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Mon, 18 Apr 2022 15:42:16 -0700 Subject: [PATCH 2/6] Pass ICE suppressions to Light (#9061) --- .../src/CreateVisualStudioWorkload.wix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ef2fb050577..c3df8721048 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -241,7 +241,7 @@ public override bool Execute() _ = Parallel.ForEach(data.FeatureBands.Keys, platform => { WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem msiOutputItem = msi.Build(MsiOutputPath); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); // Create the JSON manifest for CLI based installations. string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); From a19d70f7dfba70235bb4fb809eb01509713a88b4 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Wed, 8 Jun 2022 09:43:14 -0700 Subject: [PATCH 3/6] Create workload pack group installers (#9514) * Remove duplicate PackageReference * Create MSIs for workoad pack groups * Build NuGet wrapper packages for workload pack group MSIs * Generate WorkloadPackGroups.json in manifest MSIs * Add swix authoring for workload pack groups * De-duplicate workload pack group creation * Put braces around ProductCode and UpgradeCode registry values * Write registry keys for pack groups * Fix swix dependencies for pack groups * Use correct GUID format when setting candle variables * Add test for creating pack group dependency in SWR file --- .../Microsoft.DotNet.Build.Tasks.Feed.csproj | 1 + .../SwixComponentTests.cs | 27 ++- .../src/BuildData.wix.cs | 2 +- .../src/CreateVisualStudioWorkload.wix.cs | 144 ++++++++++++- .../src/Msi/MsiBase.wix.cs | 37 ++-- .../src/Msi/MsiMetadata.wix.cs | 94 +++++++++ .../src/Msi/MsiPayloadPackageProject.wix.cs | 4 +- .../src/Msi/WorkloadManifestMsi.wix.cs | 100 ++++++++- .../src/Msi/WorkloadPackGroupMsi.wix.cs | 195 ++++++++++++++++++ .../src/Msi/WorkloadPackMsi.wix.cs | 5 +- .../src/MsiTemplate/ManifestProduct.wxs | 3 + .../src/MsiTemplate/Registry.wxs | 2 +- .../src/Swix/SwixComponent.cs | 26 ++- .../src/Wix/CompilerToolTask.cs | 5 + .../src/Wix/HarvesterToolTask.cs | 4 +- .../src/Wix/PreprocessorDefinitionNames.cs | 1 + .../src/WorkloadPackGroupPackage.wix.cs | 43 ++++ .../src/WorkloadPackPackage.wix.cs | 2 +- 18 files changed, 640 insertions(+), 55 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/Microsoft.DotNet.Build.Tasks.Feed.csproj b/src/Microsoft.DotNet.Build.Tasks.Feed/Microsoft.DotNet.Build.Tasks.Feed.csproj index 8e357df9c94..711803adb2f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Feed/Microsoft.DotNet.Build.Tasks.Feed.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/Microsoft.DotNet.Build.Tasks.Feed.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs index 3e7fb8ea965..e468adef9c3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs @@ -23,7 +23,7 @@ public void ItAssignsDefaultValues() { WorkloadManifest manifest = Create("WorkloadManifest.json"); WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest); + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null); ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); string swixProj = project.Create(); @@ -47,7 +47,7 @@ public void ItShortensComponentIds() WorkloadManifest manifest = Create("WorkloadManifest.json"); WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, shortNames: shortNames); + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, shortNames: shortNames); ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); string swixProj = project.Create(); @@ -61,7 +61,7 @@ public void ItIgnoresNonApplicableDepedencies() { WorkloadManifest manifest = Create("AbstractWorkloadsNonWindowsPacks.json"); WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, null, null); + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, null, null); ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); string swixProj = project.Create(); @@ -91,7 +91,7 @@ public void ItCanOverrideDefaultValues() }) }; - SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, componentResources); + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, componentResources); ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); string swixProj = project.Create(); @@ -107,7 +107,7 @@ public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks() { WorkloadManifest manifest = Create("mauiWorkloadManifest.json"); WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; - SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest); + SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest, packGroupId: null); ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); string swixProj = project.Create(); @@ -116,6 +116,23 @@ public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks() Assert.Contains(@"vs.dependency id=maui.desktop", componentSwr); } + [Fact] + public void ItCreatesDependenciesForPackGroup() + { + WorkloadManifest manifest = Create("WorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + var packGroupId = "microsoft.net.sdk.blazorwebassembly.aot.WorkloadPacks"; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest, packGroupId: packGroupId); + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + + // Should have only one dependency, use string.Split to check how many times vs.dependency occurs in swr + Assert.Equal(2, componentSwr.Split(new[] { "vs.dependency" }, StringSplitOptions.None).Length); + Assert.Contains($"vs.dependency id={packGroupId}", componentSwr); + } + private static WorkloadManifest Create(string filename) { return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(filename), diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs index 3642877491a..38ae8da0597 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs @@ -21,7 +21,7 @@ public WorkloadPackPackage Package } /// - /// The set of feature bands that include contain a reference to this pack. + /// For each platform, the set of feature bands that include contain a reference to this pack. /// public Dictionary> FeatureBands = new(); 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 c3df8721048..8b33bf64190 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -146,15 +146,43 @@ public ITaskItem[] WorkloadManifestPackageFiles set; } + public bool CreateWorkloadPackGroups + { + get; + set; + } + + public bool UseWorkloadPackGroupsForVS + { + get; + set; + } + + /// + /// If true, will skip creating MSIs for workload packs if they are part of a pack group + /// + public bool SkipRedundantMsiCreation + { + get; + set; + } + + public bool DisableParallelPackageGroupProcessing + { + get; + set; + } + public override bool Execute() { try { // TODO: trim out duplicate manifests. List manifestPackages = new(); - List manifestMsisToBuild = new(); + List manifestMsisToBuild = new(); List swixComponents = 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 @@ -165,9 +193,12 @@ public override bool Execute() WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, ManifestMsiVersion, ShortNames, Log); manifestPackages.Add(manifestPackage); + Dictionary manifestMsisByPlatform = new(); foreach (string platform in SupportedPlatforms) { - manifestMsisToBuild.Add(new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath)); + var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); + manifestMsisToBuild.Add(manifestMsi); + manifestMsisByPlatform[platform] = manifestMsi; } // 2. Process the manifest itself to determine the set of packs involved and create @@ -186,6 +217,8 @@ public override bool Execute() { if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null)) { + Dictionary> packsInWorkloadByPlatform = new(); + foreach (WorkloadPackId packId in wd.Packs) { WorkloadPack pack = manifest.Packs[packId]; @@ -215,13 +248,53 @@ public override bool Execute() } _ = 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); + } + } + } + + string packGroupId = null; + + 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) + { + 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); + packGroupId = groupPackage.Id; + groupPackage.Packs.AddRange(kvp.Value); + packGroupPackages[uniquePackGroupKey] = groupPackage; + } + + if (!groupPackage.ManifestsPerPlatform.ContainsKey(platform)) + { + groupPackage.ManifestsPerPlatform[platform] = new(); + } + groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage); + + manifestMsisByPlatform[platform].WorkloadPackGroups.Add(groupPackage); } } // Finally, add a component for the workload in Visual Studio. - SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, + SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId, ComponentResources, ShortNames); swixComponents.Add(component); } @@ -247,7 +320,7 @@ public override bool Execute() string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Package, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); lock (msiItems) @@ -271,6 +344,52 @@ public override bool Execute() }); }); + + // 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) + { + pack.Extract(); + } + + foreach (var platform in packGroup.ManifestsPerPlatform.Keys) + { + WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + + // Create the JSON manifest for CLI based installations. + string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); + + // Generate a .csproj to package the MSI and its manifest for CLI installs. + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); + + lock (msiItems) + { + msiItems.Add(msiOutputItem); + } + + if (UseWorkloadPackGroupsForVS) + { + MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = swixProject.Create(); + + PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage => + { + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{manifestPackage.SdkFeatureBand}"); + + 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. @@ -292,7 +411,7 @@ public override bool Execute() } // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Package, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); lock (msiItems) @@ -326,5 +445,20 @@ public override bool Execute() return !Log.HasLoggedErrors; } + + static void PossiblyParallelForEach(bool runInParallel, IEnumerable source, Action body) + { + if (runInParallel) + { + _ = Parallel.ForEach(source, body); + } + else + { + foreach (var item in source) + { + body(item); + } + } + } } } 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 91fb78e1664..9fd267f3990 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 @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; +using NuGet.Versioning; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { @@ -36,9 +37,9 @@ internal abstract class MsiBase internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C"); /// - /// The workload package used to create the MSI. + /// Metadata for the MSI such as package ID, version, author information, etc. /// - public WorkloadPackageBase Package + public MsiMetadata Metadata { get; } @@ -68,8 +69,8 @@ protected string BaseIntermediateOutputPath /// Gets the value to use for the manufacturer. /// protected string Manufacturer => - (!string.IsNullOrWhiteSpace(Package.Authors) && (Package.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) ? - Package.Authors : + (!string.IsNullOrWhiteSpace(Metadata.Authors) && (Metadata.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) ? + Metadata.Authors : DefaultValues.Manufacturer; /// @@ -96,7 +97,7 @@ protected string WixToolsetPath get; } - public MsiBase(WorkloadPackageBase package, IBuildEngine buildEngine, string wixToolsetPath, + public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath, string platform, string baseIntermediateOutputPath) { BuildEngine = buildEngine; @@ -105,9 +106,9 @@ public MsiBase(WorkloadPackageBase package, IBuildEngine buildEngine, string wix BaseIntermediateOutputPath = baseIntermediateOutputPath; // Candle expects the output path to be terminated with a single '\'. - CompilerOutputPath = Utils.EnsureTrailingSlash(Path.Combine(baseIntermediateOutputPath, "wixobj", package.Id, $"{package.PackageVersion}", platform)); - WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", package.Id, $"{package.PackageVersion}", platform); - Package = package; + CompilerOutputPath = Utils.EnsureTrailingSlash(Path.Combine(baseIntermediateOutputPath, "wixobj", metadata.Id, $"{metadata.PackageVersion}", platform)); + WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", metadata.Id, $"{metadata.PackageVersion}", platform); + Metadata = metadata; } /// @@ -124,7 +125,7 @@ public MsiBase(WorkloadPackageBase package, IBuildEngine buildEngine, string wix /// The platform targeted by the MSI. /// A string containing the product name of the MSI. protected string GetProductName(string platform) => - (string.IsNullOrWhiteSpace(Package.Title) ? Package.Id : Package.Title) + $" ({platform})"; + (string.IsNullOrWhiteSpace(Metadata.Title) ? Metadata.Id : Metadata.Title) + $" ({platform})"; /// /// Generates a EULA (RTF file) that contains the license URL of the underlying NuGet package. @@ -132,7 +133,7 @@ protected string GetProductName(string platform) => protected string GenerateEula() { string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf"); - File.WriteAllText(eulaRtf, s_eula.Replace(__LICENSE_URL__, Package.LicenseUrl)); + File.WriteAllText(eulaRtf, s_eula.Replace(__LICENSE_URL__, Metadata.LicenseUrl)); return eulaRtf; } @@ -151,12 +152,12 @@ protected CompilerToolTask CreateDefaultCompiler() candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Package.Id); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Package.PackageVersion}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Metadata.Id); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}"); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid()}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}"); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform)); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Package.MsiVersion}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}"); return candle; } @@ -209,10 +210,10 @@ protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem TaskItem msiItem = new TaskItem(light.OutputFile); // Return a task item that contains all the information about the generated MSI. - msiItem.SetMetadata(Metadata.Platform, Platform); - msiItem.SetMetadata(Metadata.WixObj, compilerOutputPath); - msiItem.SetMetadata(Metadata.Version, $"{Package.MsiVersion}"); - msiItem.SetMetadata(Metadata.SwixPackageId, Package.SwixPackageId); + msiItem.SetMetadata(Workloads.Metadata.Platform, Platform); + msiItem.SetMetadata(Workloads.Metadata.WixObj, compilerOutputPath); + msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}"); + msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId); return msiItem; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs new file mode 100644 index 00000000000..b43bcefab05 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiMetadata.wix.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + internal class MsiMetadata + { + public string Id + { + get; + } + + public NuGetVersion PackageVersion + { + get; + } + + public Version MsiVersion + { + get; + } + + public string Authors + { + get; + } + + public string Copyright + { + get; + } + + public string Description + { + get; + } + + public string Title + { + get; + } + + public string LicenseUrl + { + get; + } + public string ProjectUrl + { + get; + } + + public string SwixPackageId + { + get; + } + + public MsiMetadata(string id, NuGetVersion packageVersion, Version msiVersion, string authors, string copyright, string description, string title, string licenseUrl, string projectUrl, string swixPackageId) + { + Id = id; + PackageVersion = packageVersion; + MsiVersion = msiVersion; + Authors = authors; + Copyright = copyright; + Description = description; + Title = title; + LicenseUrl = licenseUrl; + ProjectUrl = projectUrl; + SwixPackageId = swixPackageId; + } + + public static MsiMetadata Create(WorkloadPackageBase package) + { + return new( + package.Id, + package.PackageVersion, + package.MsiVersion, + package.Authors, + package.Copyright, + package.Description, + package.Title, + package.LicenseUrl, + package.ProjectUrl, + package.SwixPackageId + ); + } + } +} + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs index 45a83180c29..b253a4afbc6 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -26,7 +26,7 @@ protected override string ProjectFile get; } - public MsiPayloadPackageProject(WorkloadPackageBase package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, string msiJsonPath) : + public MsiPayloadPackageProject(MsiMetadata package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, string msiJsonPath) : base(baseIntermediateOutputPath, baseOutputPath) { string platform = msi.GetMetadata(Metadata.Platform); 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 a0714d20a68..78aac34fbd8 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 @@ -4,7 +4,10 @@ #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; @@ -15,18 +18,20 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi /// internal class WorkloadManifestMsi : MsiBase { - private WorkloadManifestPackage _package; + public WorkloadManifestPackage Package { get; } /// /// The directory reference to use when harvesting the package contents. /// private static readonly string ManifestIdDirectory = "ManifestIdDir"; + public List WorkloadPackGroups { get; } = new(); + public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, string baseIntermediateOutputPath) : - base(package, buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath) + base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath) { - _package = package; + Package = package; } /// @@ -35,7 +40,7 @@ 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"); + string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data"); HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) { @@ -50,12 +55,69 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions throw new Exception(Strings.HeatFailedToHarvest); } + // Add WorkloadPackGroups.json to add to workload manifest MSI + string? jsonContentWxs = null; + string? jsonDirectory = null; + + if (WorkloadPackGroups.Any()) + { + jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs"); + + List packGroupListJson = new List(); + foreach (var packGroup in WorkloadPackGroups) + { + var json = new WorkloadPackGroupJson() + { + GroupPackageId = packGroup.Id, + GroupPackageVersion = packGroup.GetMsiMetadata().PackageVersion.ToString() + }; + json.Packs.AddRange(packGroup.Packs.Select(p => new WorkloadPackJson() + { + PackId = p.Id, + PackVersion = p.PackageVersion.ToString() + })); + + packGroupListJson.Add(json); + } + + string jsonAsString = JsonSerializer.Serialize(packGroupListJson, typeof(IList), new JsonSerializerOptions() { WriteIndented = true }); + jsonDirectory = Path.Combine(WixSourceDirectory, "json"); + Directory.CreateDirectory(jsonDirectory); + File.WriteAllText(Path.Combine(jsonDirectory, "WorkloadPackGroups.json"), jsonAsString); + + HarvesterToolTask jsonHeat = new(BuildEngine, WixToolsetPath) + { + DirectoryReference = ManifestIdDirectory, + OutputFile = jsonContentWxs, + Platform = this.Platform, + SourceDirectory = jsonDirectory, + SourceVariableName = "JsonSourceDir", + ComponentGroupName = "CG_PackGroupJson" + }; + + if (!jsonHeat.Execute()) + { + throw new Exception(Strings.HeatFailedToHarvest); + } + } + CompilerToolTask candle = CreateDefaultCompiler(); candle.AddSourceFiles(packageContentWxs, EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory), EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory), EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory)); + if (jsonContentWxs != null) + { + candle.AddSourceFiles(jsonContentWxs); + candle.AddPreprocessorDefinition("IncludePackGroupJson", "true"); + candle.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory); + } + else + { + candle.AddPreprocessorDefinition("IncludePackGroupJson", "false"); + } + // Only extract the include file as it's not compilable, but imported by various source files. EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); @@ -63,18 +125,18 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. // The workload author will need to guarantee that the version for the MSI is higher than previous shipped versions // to ensure upgrades correctly trigger. - Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.ManifestId};{_package.SdkFeatureBand};{Platform}"); - string providerKeyName = $"{_package.ManifestId},{_package.SdkFeatureBand},{Platform}"; + Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}"); + string providerKeyName = $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}"; // Set up additional preprocessor definitions. - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode}"); + 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.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. // We have to do the same to ensure the keypath generation produces stable GUIDs. - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{_package.ManifestId.ToLowerInvariant()}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}"); if (!candle.Execute()) { @@ -82,11 +144,27 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions } ITaskItem msi = Link(candle.OutputPath, - Path.Combine(outputPath, Path.GetFileNameWithoutExtension(_package.PackagePath) + $"-{Platform}.msi"), + Path.Combine(outputPath, Path.GetFileNameWithoutExtension(Package.PackagePath) + $"-{Platform}.msi"), iceSuppressions); return msi; - } + } + + + class WorkloadPackGroupJson + { + public string? GroupPackageId { get; set; } + public string? GroupPackageVersion { get; set; } + + public List Packs { get; set; } = new List(); + } + + class WorkloadPackJson + { + public string? PackId { get; set; } + + public string? PackVersion { get; set; } + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs new file mode 100644 index 00000000000..00946f5e721 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + internal class WorkloadPackGroupMsi : MsiBase + { + WorkloadPackGroupPackage _package; + + public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, + string baseIntermediatOutputPath) + : base(package.GetMsiMetadata(), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + { + _package = package; + } + + public override ITaskItem Build(string outputPath, ITaskItem[] iceSuppressions) + { + List packageContentWxsFiles = new List(); + + int packNumber = 1; + + MsiDirectory dotnetHomeDirectory = new MsiDirectory("dotnet", "DOTNETHOME"); + Dictionary sourceDirectoryNamesAndValues = new(); + + foreach (var pack in _package.Packs) + { + string packageContentWxs = Path.Combine(WixSourceDirectory, $"PackageContent.{pack.Id}.wxs"); + + string directoryReference; + if (pack.Kind == WorkloadPackKind.Library) + { + directoryReference = dotnetHomeDirectory.GetSubdirectory("library-packs", "LibraryPacksDir").Id; + } + else if (pack.Kind == WorkloadPackKind.Template) + { + directoryReference = dotnetHomeDirectory.GetSubdirectory("template-packs", "TemplatePacksDir").Id; + } + else + { + var versionDir = dotnetHomeDirectory.GetSubdirectory("packs", "PacksDir") + .GetSubdirectory(pack.Id, "PackDir" + packNumber) + .GetSubdirectory($"{pack.PackageVersion}", "PackVersionDir" + packNumber); + + directoryReference = versionDir.Id; + } + + HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) + { + DirectoryReference = directoryReference, + OutputFile = packageContentWxs, + Platform = this.Platform, + SourceDirectory = pack.DestinationDirectory, + SourceVariableName = "SourceDir" + packNumber, + ComponentGroupName = "CG_PackageContents" + packNumber + }; + + sourceDirectoryNamesAndValues[heat.SourceVariableName] = heat.SourceDirectory; + + if (!heat.Execute()) + { + throw new Exception(Strings.HeatFailedToHarvest); + } + + packageContentWxsFiles.Add(packageContentWxs); + + packNumber++; + } + + // Create wxs file from dotnetHomeDirectory structure + string directoriesWxsPath = EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + var directoriesDoc = XDocument.Load(directoriesWxsPath); + var dotnetHomeElement = directoriesDoc.Root.Descendants().Where(d => (string)d.Attribute("Id") == "DOTNETHOME").Single(); + // Remove existing subfolders of DOTNETHOME, which are for single pack MSI + dotnetHomeElement.ReplaceWith(dotnetHomeDirectory.ToXml()); + directoriesDoc.Save(directoriesWxsPath); + + // Replace single ComponentGroupRef from Product.wxs with a ref for each pack + string productWxsPath = EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory); + var productDoc = XDocument.Load(productWxsPath); + var ns = productDoc.Root.Name.Namespace; + var componentGroupRefElement = productDoc.Root.Descendants(ns + "ComponentGroupRef").Single(); + componentGroupRefElement.ReplaceWith(Enumerable.Range(1, _package.Packs.Count).Select(n => new XElement(ns + "ComponentGroupRef", new XAttribute("Id", "CG_PackageContents" + n)))); + productDoc.Save(productWxsPath); + + // Add registry keys for packs in the pack group. + string registryWxsPath = EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + var registryDoc = XDocument.Load(registryWxsPath); + ns = registryDoc.Root.Name.Namespace; + var registryKeyElement = registryDoc.Root.Descendants(ns + "RegistryKey").Single(); + foreach (var pack in _package.Packs) + { + registryKeyElement.Add(new XElement(ns + "RegistryKey", new XAttribute("Key", pack.Id), + new XElement(ns + "RegistryKey", new XAttribute("Key", pack.PackageVersion), + new XElement(ns + "RegistryValue", new XAttribute("Value", ""), new XAttribute("Type", "string"))))); + } + registryDoc.Save(registryWxsPath); + + CompilerToolTask candle = CreateDefaultCompiler(); + + candle.AddSourceFiles(packageContentWxsFiles); + + candle.AddSourceFiles( + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory), + directoriesWxsPath, + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory), + productWxsPath, + registryWxsPath); + + // Only extract the include file as it's not compilable, but imported by various source files. + EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); + + // Workload packs are not upgradable so the upgrade code is generated using the package identity as that + // includes the package version. + Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}"); + string providerKeyName = $"{_package.Id},{Metadata.PackageVersion},{Platform}"; + + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPackGroups"); + foreach (var kvp in sourceDirectoryNamesAndValues) + { + candle.AddPreprocessorDefinition(kvp.Key, kvp.Value); + } + + if (!candle.Execute()) + { + throw new Exception(Strings.FailedToCompileMsi); + } + + string msiFileName = Path.Combine(outputPath, Metadata.Id + $"-{Platform}.msi"); + + ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + + return msi; + + } + + class MsiDirectory + { + public string Name { get; } + public string Id { get; } + + public Dictionary Subdirectories { get; } = new(); + + public MsiDirectory(string name, string id) + { + Name = name; + Id = id; + } + + public MsiDirectory GetSubdirectory(string name, string id) + { + if (Subdirectories.TryGetValue(name, out var subdir)) + { + if (!subdir.Id.Equals(id, StringComparison.Ordinal)) + { + throw new ArgumentException($"ID {id} didn't match existing ID {subdir.Id} for directory {name}."); + } + return subdir; + } + + subdir = new MsiDirectory(name, id); + Subdirectories.Add(name, subdir); + return subdir; + } + + public XElement ToXml() + { + XNamespace ns = "http://schemas.microsoft.com/wix/2006/wi"; + var xml = new XElement(ns + "Directory"); + xml.SetAttributeValue("Id", Id); + xml.SetAttributeValue("Name", Name); + + foreach (var subdir in Subdirectories.Values) + { + xml.Add(subdir.ToXml()); + } + + return xml; + } + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 32a82891192..8d9ca0ba285 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -17,7 +17,7 @@ internal class WorkloadPackMsi : MsiBase public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, string baseIntermediatOutputPath) : - base(package, buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) { _package = package; } @@ -60,10 +60,11 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions string providerKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}"; candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); + candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPacks"); if (!candle.Execute()) { 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 13c2eadf30f..ae9196db578 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -47,6 +47,9 @@ + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs index 8a206723b47..30bb5ead5b8 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs @@ -6,7 +6,7 @@ - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs index 10cc3aff0f1..28ed9debe7f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs @@ -158,11 +158,13 @@ public void AddDependency(WorkloadPack pack) /// The SDK featureband associated with the workload manifest. /// The workload definition to use for the component. /// The workload manifest to which the workload belongs. + /// The ID of a workload pack group to add as a dependency instead of individual packs /// Additional resources that can be used to override component attributes such /// as the title, description, and category. /// A set of items used to shorten the names of setup packages. /// A SWIX component. public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefinition workload, WorkloadManifest manifest, + string? packGroupId, ITaskItem[]? componentResources = null, ITaskItem[]? shortNames = null) { ITaskItem? resourceItem = componentResources?.Where(r => string.Equals(r.ItemSpec, workload.Id)).FirstOrDefault(); @@ -191,18 +193,26 @@ public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefini // TODO: Check for missing packs - foreach (WorkloadPackId packId in workload.Packs ?? Enumerable.Empty()) + if (packGroupId != null) { - // Check whether the pack dependency is aliased to non-Windows RIDs. If so, we can't add a dependency for the pack - // because we won't be able to create any installers. - if (manifest.Packs.TryGetValue(packId, out WorkloadPack? pack)) + // Add dependency to workload pack group + component.AddDependency(packGroupId, new NuGetVersion(manifest.Version).Version, maxVersion: null); + } + else + { + foreach (WorkloadPackId packId in workload.Packs ?? Enumerable.Empty()) { - if (pack.IsAlias && pack.AliasTo != null && !pack.AliasTo.Keys.Any(rid => s_SupportedRids.Contains(rid))) + // Check whether the pack dependency is aliased to non-Windows RIDs. If so, we can't add a dependency for the pack + // because we won't be able to create any installers. + if (manifest.Packs.TryGetValue(packId, out WorkloadPack? pack)) { - continue; - } + if (pack.IsAlias && pack.AliasTo != null && !pack.AliasTo.Keys.Any(rid => s_SupportedRids.Contains(rid))) + { + continue; + } - component.AddDependency(manifest.Packs[packId]); + component.AddDependency(manifest.Packs[packId]); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs index b3425c3a30b..8421a182126 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs @@ -63,6 +63,11 @@ public void AddSourceFiles(params string[] sourceFiles) } } + public void AddSourceFiles(IEnumerable sourceFiles) + { + _sourceFiles.AddRange(sourceFiles); + } + /// protected override string GenerateCommandLineCommands() { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs index e51f3403766..2526f0246d1 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs @@ -87,6 +87,8 @@ public HeatSuppressions Suppressions set; } = HeatSuppressions.SuppressRegistryHarvesting | HeatSuppressions.SuppressRootDirectory; + public string SourceVariableName { get; set; } = "SourceDir"; + /// /// The name of the WiX harvest too. /// @@ -110,7 +112,7 @@ protected override string GenerateCommandLineCommands() CommandLineBuilder.AppendSwitchIfNotNull("-cg ", ComponentGroupName); // Override File/@Source="SourceDir" with a preprocessor variable, $(var.SourceDir) - CommandLineBuilder.AppendSwitch("-var var.SourceDir"); + CommandLineBuilder.AppendSwitch($"-var var.{SourceVariableName}"); // GUID generation if (GenerateGuids == GuidOptions.GenerateAtCompileTime) 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 19b777c939b..413c4a0702c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs @@ -11,6 +11,7 @@ public static class PreprocessorDefinitionNames public static readonly string DependencyProviderKeyName = nameof(DependencyProviderKeyName); public static readonly string EulaRtf = nameof(EulaRtf); public static readonly string InstallDir = nameof(InstallDir); + public static readonly string InstallationRecordKey = nameof(InstallationRecordKey); public static readonly string ManifestId = nameof(ManifestId); public static readonly string Manufacturer = nameof(Manufacturer); public static readonly string PackKind = nameof(PackKind); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs new file mode 100644 index 00000000000..f4756c9db6f --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs @@ -0,0 +1,43 @@ +// 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.DotNet.Build.Tasks.Workloads.Msi; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + internal class WorkloadPackGroupPackage + { + public List Packs { get; set; } = new(); + + public Dictionary> ManifestsPerPlatform { get; } = new(); + + public string WorkloadName { get; } + + public string Id { get; } + + public WorkloadPackGroupPackage(string workloadName) + { + WorkloadName = workloadName; + Id = Utils.ToSafeId(workloadName) + ".WorkloadPacks"; + } + + public MsiMetadata GetMsiMetadata() + { + // Take latest manifest from arbitrary platform to use for metadata + var manifestPackage = ManifestsPerPlatform.First().Value.OrderBy(m => m.Version).Last(); + + return new MsiMetadata(Id, manifestPackage.PackageVersion, manifestPackage.MsiVersion, manifestPackage.Authors, + manifestPackage.Copyright, + description: "Workload packs for " + WorkloadName, + title: "Workload packs for " + WorkloadName, + manifestPackage.LicenseUrl, + manifestPackage.ProjectUrl, + swixPackageId: Id); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs index 2b09a70914f..e8002b061bc 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs @@ -48,7 +48,7 @@ public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platf /// /// /// An enumerable of tuples. Each tuple contains the full path of the NuGet package and support platforms. - internal static IEnumerable<(string, string[])> GetSourcePackages(string packageSource, WorkloadPack pack) + internal static IEnumerable<(string sourcePackage, string[] platforms)> GetSourcePackages(string packageSource, WorkloadPack pack) { if (pack.IsAlias && pack.AliasTo != null) { From 1421dd6f9c6469e427cd7af4d095b6806d97893f Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Mon, 27 Jun 2022 17:13:28 -0700 Subject: [PATCH 4/6] Support building with missing workload packs (#9628) * Support building with missing workload packs * Include extracted manifest files in manifest MSI payload nupkg --- .../src/CreateVisualStudioWorkload.wix.cs | 68 ++++++++++++++----- .../src/Misc/msi.csproj | 3 - .../src/Msi/MsiBase.wix.cs | 14 ++++ .../src/Msi/MsiPayloadPackageProject.wix.cs | 23 +++++-- .../src/Msi/PayloadPackageTokens.wix.cs | 5 +- .../src/Msi/WorkloadManifestMsi.wix.cs | 39 +++++------ .../src/Msi/WorkloadPackGroupMsi.wix.cs | 2 + .../src/Msi/WorkloadPackMsi.wix.cs | 2 + .../src/WorkloadPackGroupPackage.wix.cs | 7 +- 9 files changed, 113 insertions(+), 50 deletions(-) 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 8b33bf64190..a8eb843afaf 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -11,6 +11,7 @@ using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.DotNet.Build.Tasks.Workloads.Swix; using Microsoft.NET.Sdk.WorkloadManifestReader; +using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; using Parallel = System.Threading.Tasks.Parallel; namespace Microsoft.DotNet.Build.Tasks.Workloads @@ -173,6 +174,15 @@ public bool DisableParallelPackageGroupProcessing set; } + /// + /// Allow VS workload generation to proceed if any nupkgs declared in the manifest are not found on disk. + /// + public bool AllowMissingPacks + { + get; + set; + } = false; + public override bool Execute() { try @@ -213,21 +223,54 @@ public override bool Execute() // 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)) { Dictionary> packsInWorkloadByPlatform = new(); + string packGroupId = null; + WorkloadPackGroupJson packGroupJson = null; + if (CreateWorkloadPackGroups) + { + packGroupId = WorkloadPackGroupPackage.GetPackGroupID(workload.Id); + packGroupJson = new WorkloadPackGroupJson() + { + GroupPackageId = packGroupId, + GroupPackageVersion = manifestPackage.PackageVersion.ToString() + }; + packGroupJsonList.Add(packGroupJson); + } + + foreach (WorkloadPackId packId in wd.Packs) { WorkloadPack pack = manifest.Packs[packId]; + if (CreateWorkloadPackGroups) + { + packGroupJson.Packs.Add(new WorkloadPackJson() + { + PackId = pack.Id, + PackVersion = pack.Version + }); + } + foreach ((string sourcePackage, string[] platforms) in WorkloadPackPackage.GetSourcePackages(PackageSource, pack)) { if (!File.Exists(sourcePackage)) { - throw new FileNotFoundException(message: null, fileName: 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. @@ -264,7 +307,7 @@ public override bool Execute() } } - string packGroupId = null; + if (CreateWorkloadPackGroups) { @@ -278,7 +321,6 @@ public override bool Execute() if (!packGroupPackages.TryGetValue(uniquePackGroupKey, out var groupPackage)) { groupPackage = new WorkloadPackGroupPackage(workload.Id); - packGroupId = groupPackage.Id; groupPackage.Packs.AddRange(kvp.Value); packGroupPackages[uniquePackGroupKey] = groupPackage; } @@ -288,8 +330,11 @@ public override bool Execute() groupPackage.ManifestsPerPlatform[platform] = new(); } groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage); + } - manifestMsisByPlatform[platform].WorkloadPackGroups.Add(groupPackage); + foreach (var manifestMsi in manifestMsisByPlatform.Values) + { + manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList); } } @@ -316,11 +361,8 @@ public override bool Execute() WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); - // Create the JSON manifest for CLI based installations. - string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); - // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); lock (msiItems) @@ -359,11 +401,8 @@ public override bool Execute() WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); - // Create the JSON manifest for CLI based installations. - string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); - // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); lock (msiItems) @@ -397,9 +436,6 @@ public override bool Execute() { ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); - // Create the JSON manifest for CLI based installations. - string msiJsonPath = MsiProperties.Create(msiOutputItem.ItemSpec); - // Generate SWIX authoring for the MSI package. MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); @@ -411,7 +447,7 @@ public override bool Execute() } // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, Path.GetFullPath(msiJsonPath)); + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); lock (msiItems) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj index 549eecf737a..fe29ece35a1 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj @@ -24,9 +24,6 @@ - - - NuGetPackageFiles { get; set; } = new(); + public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath, string platform, string baseIntermediateOutputPath) { @@ -217,6 +220,17 @@ protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem return msiItem; } + + protected void AddDefaultPackageFiles(ITaskItem msi) + { + NuGetPackageFiles[msi.GetMetadata(Workloads.Metadata.FullPath)] = @"\data"; + + // Create the JSON manifest for CLI based installations. + string msiJsonPath = MsiProperties.Create(msi.ItemSpec); + NuGetPackageFiles[Path.GetFullPath(msiJsonPath)] = "\\data\\msi.json"; + + NuGetPackageFiles["LICENSE.TXT"] = @"\"; + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs index b253a4afbc6..9f09b8a2246 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Xml.Linq; using Microsoft.Build.Framework; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi @@ -26,22 +27,24 @@ protected override string ProjectFile get; } - public MsiPayloadPackageProject(MsiMetadata package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, string msiJsonPath) : + // Key: path to file, value: path in package + public Dictionary PackageContents { get; set; } = new(); + + public MsiPayloadPackageProject(MsiMetadata package, ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, Dictionary packageContents) : base(baseIntermediateOutputPath, baseOutputPath) { string platform = msi.GetMetadata(Metadata.Platform); ProjectSourceDirectory = Path.Combine(SourceDirectory, "msiPackage", platform, package.Id); ProjectFile = "msi.csproj"; + PackageContents = packageContents; + ReplacementTokens[PayloadPackageTokens.__AUTHORS__] = package.Authors; ReplacementTokens[PayloadPackageTokens.__COPYRIGHT__] = package.Copyright; ReplacementTokens[PayloadPackageTokens.__DESCRIPTION__] = package.Description; ReplacementTokens[PayloadPackageTokens.__PACKAGE_ID__] = $"{package.Id}.Msi.{platform}"; ReplacementTokens[PayloadPackageTokens.__PACKAGE_PROJECT_URL__] = package.ProjectUrl; ReplacementTokens[PayloadPackageTokens.__PACKAGE_VERSION__] = $"{package.PackageVersion}"; - ReplacementTokens[PayloadPackageTokens.__MSI__] = msi.GetMetadata(Metadata.FullPath); - ReplacementTokens[PayloadPackageTokens.__MSI_JSON__] = msiJsonPath; - ReplacementTokens[PayloadPackageTokens.__LICENSE_FILENAME__] = "LICENSE.TXT"; } /// @@ -50,6 +53,18 @@ public override string Create() string msiCsproj = EmbeddedTemplates.Extract("msi.csproj", ProjectSourceDirectory); Utils.StringReplace(msiCsproj, ReplacementTokens, Encoding.UTF8); + + var proj = XDocument.Load(msiCsproj); + var itemGroup = proj.Root.Element("ItemGroup"); + foreach (var packageFile in PackageContents) + { + itemGroup.Add(new XElement("None", + new XAttribute("Include", packageFile.Key), + new XAttribute("Pack", "true"), + new XAttribute("PackagePath", packageFile.Value))); + } + proj.Save(msiCsproj); + EmbeddedTemplates.Extract("Icon.png", ProjectSourceDirectory); EmbeddedTemplates.Extract("LICENSE.TXT", ProjectSourceDirectory); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs index 13755c1c448..18610e361c3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -12,9 +12,6 @@ internal static class PayloadPackageTokens public static readonly string __AUTHORS__ = nameof(__AUTHORS__); public static readonly string __COPYRIGHT__ = nameof(__COPYRIGHT__); public static readonly string __DESCRIPTION__ = nameof(__DESCRIPTION__); - public static readonly string __LICENSE_FILENAME__ = nameof(__LICENSE_FILENAME__); - public static readonly string __MSI__ = nameof(__MSI__); - public static readonly string __MSI_JSON__ = nameof(__MSI_JSON__); public static readonly string __PACKAGE_ID__ = nameof(__PACKAGE_ID__); public static readonly string __PACKAGE_PROJECT_URL__ = nameof(__PACKAGE_PROJECT_URL__); public static readonly string __PACKAGE_VERSION__ = nameof(__PACKAGE_VERSION__); 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 78aac34fbd8..07d9b193c90 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 @@ -25,7 +25,8 @@ internal class WorkloadManifestMsi : MsiBase /// private static readonly string ManifestIdDirectory = "ManifestIdDir"; - public List WorkloadPackGroups { get; } = new(); + public List WorkloadPackGroups { get; } = new(); + public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, string baseIntermediateOutputPath) : @@ -55,6 +56,11 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions throw new Exception(Strings.HeatFailedToHarvest); } + foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f))) + { + NuGetPackageFiles[file] = @"\data\extractedManifest\" + Path.GetFileName(file); + } + // Add WorkloadPackGroups.json to add to workload manifest MSI string? jsonContentWxs = null; string? jsonDirectory = null; @@ -63,27 +69,12 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions { jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs"); - List packGroupListJson = new List(); - foreach (var packGroup in WorkloadPackGroups) - { - var json = new WorkloadPackGroupJson() - { - GroupPackageId = packGroup.Id, - GroupPackageVersion = packGroup.GetMsiMetadata().PackageVersion.ToString() - }; - json.Packs.AddRange(packGroup.Packs.Select(p => new WorkloadPackJson() - { - PackId = p.Id, - PackVersion = p.PackageVersion.ToString() - })); - - packGroupListJson.Add(json); - } - - string jsonAsString = JsonSerializer.Serialize(packGroupListJson, typeof(IList), new JsonSerializerOptions() { WriteIndented = true }); + string jsonAsString = JsonSerializer.Serialize(WorkloadPackGroups, typeof(IList), new JsonSerializerOptions() { WriteIndented = true }); jsonDirectory = Path.Combine(WixSourceDirectory, "json"); Directory.CreateDirectory(jsonDirectory); - File.WriteAllText(Path.Combine(jsonDirectory, "WorkloadPackGroups.json"), jsonAsString); + + string jsonFullPath = Path.GetFullPath(Path.Combine(jsonDirectory, "WorkloadPackGroups.json")); + File.WriteAllText(jsonFullPath, jsonAsString); HarvesterToolTask jsonHeat = new(BuildEngine, WixToolsetPath) { @@ -99,6 +90,8 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions { throw new Exception(Strings.HeatFailedToHarvest); } + + NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); } CompilerToolTask candle = CreateDefaultCompiler(); @@ -146,12 +139,14 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, Path.GetFileNameWithoutExtension(Package.PackagePath) + $"-{Platform}.msi"), iceSuppressions); + + AddDefaultPackageFiles(msi); return msi; } - class WorkloadPackGroupJson + public class WorkloadPackGroupJson { public string? GroupPackageId { get; set; } public string? GroupPackageVersion { get; set; } @@ -159,7 +154,7 @@ class WorkloadPackGroupJson public List Packs { get; set; } = new List(); } - class WorkloadPackJson + public class WorkloadPackJson { public string? PackId { get; set; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index 00946f5e721..e8e9d4b982f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -143,6 +143,8 @@ public override ITaskItem Build(string outputPath, ITaskItem[] iceSuppressions) ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + AddDefaultPackageFiles(msi); + return msi; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 8d9ca0ba285..6e4f31a6164 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -78,6 +78,8 @@ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + AddDefaultPackageFiles(msi); + return msi; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs index f4756c9db6f..d4192a1d38c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs @@ -23,7 +23,12 @@ internal class WorkloadPackGroupPackage public WorkloadPackGroupPackage(string workloadName) { WorkloadName = workloadName; - Id = Utils.ToSafeId(workloadName) + ".WorkloadPacks"; + Id = GetPackGroupID(workloadName); + } + + public static string GetPackGroupID(string workloadName) + { + return Utils.ToSafeId(workloadName) + ".WorkloadPacks"; } public MsiMetadata GetMsiMetadata() From 4e67eed6890cca944f6187dc7f48060b7948fdb1 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Thu, 11 Aug 2022 10:19:02 -0700 Subject: [PATCH 5/6] Fix versioning errors in workloads (#10363) * Fix versioning errors in workloads * Disable TRX tests while reporting to AZDO is broken (https://github.com/dotnet/arcade/issues/10358) (#10380) Co-authored-by: Matt Galbraith --- .../SwixPackageTests.cs | 28 +++++++++++++++---- .../src/CreateVisualStudioWorkload.wix.cs | 4 +-- .../src/WorkloadPackageBase.cs | 2 +- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs index af08ce74b72..9c48496b694 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs @@ -2,13 +2,13 @@ // 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.Build.Utilities; +using System.IO; +using Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.DotNet.Build.Tasks.Workloads.Swix; +using Microsoft.NET.Sdk.WorkloadManifestReader; using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests @@ -30,5 +30,23 @@ 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\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)); + } } } 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 a8eb843afaf..c59bb2dc4ce 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -69,7 +69,7 @@ public ITaskItem[] IceSuppressions /// /// The version to assign to workload manifest installers. /// - public Version ManifestMsiVersion + public string ManifestMsiVersion { get; set; @@ -200,7 +200,7 @@ public override bool Execute() foreach (ITaskItem workloadManifestPackageFile in WorkloadManifestPackageFiles) { // 1. Process the manifest package and create a set of installers. - WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, ManifestMsiVersion, ShortNames, Log); + WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, new Version(ManifestMsiVersion), ShortNames, Log); manifestPackages.Add(manifestPackage); Dictionary manifestMsisByPlatform = new(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs index 55005cd3815..96b5640367c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs @@ -185,7 +185,7 @@ public WorkloadPackageBase(string packagePath, string destinationBaseDirectory, PackageFileName = Path.GetFileNameWithoutExtension(packagePath); ShortName = PackageFileName.Replace(shortNames); - SwixPackageId = Id.Replace(shortNames); + SwixPackageId = $"{Id.Replace(shortNames)}.{Identity.Version}"; Log = log; } From 21e3a349b28a4147609492c66d4495317151baf0 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Fri, 12 Aug 2022 14:01:20 -0700 Subject: [PATCH 6/6] clean up, api changes --- .../Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj | 1 - .../SwixComponentTests.cs | 2 +- .../src/WorkloadManifestPackage.wix.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) 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 b9c895b06cd..51a60494ffc 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 @@ -14,7 +14,6 @@ - diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs index e468adef9c3..d1ddd8f070c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs @@ -136,7 +136,7 @@ public void ItCreatesDependenciesForPackGroup() private static WorkloadManifest Create(string filename) { return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(filename), - File.OpenRead(Path.Combine(TestAssetsPath, filename))); + File.OpenRead(Path.Combine(TestAssetsPath, filename)), filename); } } } 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 7b41b35ab5e..e2747c4f24c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs @@ -118,7 +118,7 @@ public WorkloadManifest GetManifest() { string workloadManifestFile = GetManifestFile(); - return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(workloadManifestFile), File.OpenRead(workloadManifestFile)); + return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(workloadManifestFile), File.OpenRead(workloadManifestFile), workloadManifestFile); } ///