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/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..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
@@ -1,4 +1,4 @@
-
+
net472;$(TargetFrameworkForNETSDK)
@@ -14,11 +14,17 @@
-
+
+
+
+
-
+
+
+
+
@@ -26,4 +32,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..d1ddd8f070c
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs
@@ -0,0 +1,142 @@
+// 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, packGroupId: 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.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, packGroupId: null, 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, packGroupId: null, 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, packGroupId: null, 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, packGroupId: null);
+ 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);
+ }
+
+ [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),
+ File.OpenRead(Path.Combine(TestAssetsPath, filename)), 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..9c48496b694
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Microsoft.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
+{
+ 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);
+ }
+
+ [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.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..38ae8da0597
--- /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;
+ }
+
+ ///
+ /// For each platform, 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..c59bb2dc4ce
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
@@ -0,0 +1,500 @@
+// 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 static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi;
+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 string 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 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;
+ }
+
+ ///
+ /// 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
+ {
+ // TODO: trim out duplicate manifests.
+ List manifestPackages = 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
+ // 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, new Version(ManifestMsiVersion), ShortNames, Log);
+ manifestPackages.Add(manifestPackage);
+
+ Dictionary manifestMsisByPlatform = new();
+ foreach (string platform in SupportedPlatforms)
+ {
+ 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
+ // installers for all the packs. Duplicate packs will be ignored, example, when
+ // workloads in two manifests targeting different feature bands contain the
+ // same pack dependencies. Building multiple copies of MSIs will cause
+ // problems (ref counting, repair operations, etc.) and also increases the build time.
+ //
+ // When building multiple manifests, it's possible for feature bands to have
+ // different sets of packs. For example, the same manifest for different feature bands
+ // can add additional platforms that requires generating additional SWIX projects, while
+ // ensuring that the pack and MSI is only generated once.
+ WorkloadManifest manifest = manifestPackage.GetManifest();
+
+ List packGroupJsonList = new();
+
+ foreach (WorkloadDefinition workload in manifest.Workloads.Values)
+ {
+ if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null))
+ {
+ 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))
+ {
+ if (AllowMissingPacks)
+ {
+ Log.LogMessage($"Pack {sourcePackage} - {string.Join(",", platforms)} could not be found, it will be skipped.");
+ continue;
+ }
+ else
+ {
+ throw new FileNotFoundException(message: "NuGet package not found", fileName: sourcePackage);
+ }
+ }
+
+ // Create new build data and add the pack if we haven't seen it previously.
+ if (!buildData.ContainsKey(sourcePackage))
+ {
+ buildData[sourcePackage] = new BuildData(WorkloadPackPackage.Create(pack, sourcePackage, platforms, PackageRootDirectory,
+ ShortNames, Log));
+ }
+
+ 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);
+
+ 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);
+ }
+ }
+ }
+
+
+
+ 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);
+ groupPackage.Packs.AddRange(kvp.Value);
+ packGroupPackages[uniquePackGroupKey] = groupPackage;
+ }
+
+ if (!groupPackage.ManifestsPerPlatform.ContainsKey(platform))
+ {
+ groupPackage.ManifestsPerPlatform[platform] = new();
+ }
+ groupPackage.ManifestsPerPlatform[platform].Add(manifestPackage);
+ }
+
+ foreach (var manifestMsi in manifestMsisByPlatform.Values)
+ {
+ manifestMsi.WorkloadPackGroups.AddRange(packGroupJsonList);
+ }
+ }
+
+ // Finally, add a component for the workload in Visual Studio.
+ SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId,
+ 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, IceSuppressions);
+
+ // Generate a .csproj to package the MSI and its manifest for CLI installs.
+ MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
+ msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
+
+ lock (msiItems)
+ {
+ msiItems.Add(msiOutputItem);
+ }
+
+ 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);
+ }
+ }
+ });
+ });
+
+
+ // 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);
+
+ // Generate a .csproj to package the MSI and its manifest for CLI installs.
+ MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
+ msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create());
+
+ lock (msiItems)
+ {
+ msiItems.Add(msiOutputItem);
+ }
+
+ if (UseWorkloadPackGroupsForVS)
+ {
+ 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.
+ _ = Parallel.ForEach(manifestMsisToBuild, msi =>
+ {
+ ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions);
+
+ // 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.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles);
+ 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;
+ }
+
+ 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/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..fe29ece35a1
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Misc/msi.csproj
@@ -0,0 +1,39 @@
+
+
+
+ <_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..e7f36365c8e
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs
@@ -0,0 +1,237 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
+using NuGet.Versioning;
+
+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");
+
+ ///
+ /// Metadata for the MSI such as package ID, version, author information, etc.
+ ///
+ public MsiMetadata Metadata
+ {
+ 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(Metadata.Authors) && (Metadata.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) ?
+ Metadata.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 Dictionary NuGetPackageFiles { get; set; } = new();
+
+ public MsiBase(MsiMetadata metadata, 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", metadata.Id, $"{metadata.PackageVersion}", platform));
+ WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", metadata.Id, $"{metadata.PackageVersion}", platform);
+ Metadata = metadata;
+ }
+
+ ///
+ /// 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(Metadata.Title) ? Metadata.Id : Metadata.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__, Metadata.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, Metadata.Id);
+ candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}");
+ candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform);
+ candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}");
+ candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform));
+ candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}");
+
+ return candle;
+ }
+
+ ///
+ /// 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(Workloads.Metadata.Platform, Platform);
+ msiItem.SetMetadata(Workloads.Metadata.WixObj, compilerOutputPath);
+ msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}");
+ msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId);
+
+ return msiItem;
+ }
+
+ protected void AddDefaultPackageFiles(ITaskItem msi)
+ {
+ NuGetPackageFiles[msi.GetMetadata(Workloads.Metadata.FullPath)] = @"\data";
+
+ // Create the JSON manifest for CLI based installations.
+ string msiJsonPath = MsiProperties.Create(msi.ItemSpec);
+ NuGetPackageFiles[Path.GetFullPath(msiJsonPath)] = "\\data\\msi.json";
+
+ NuGetPackageFiles["LICENSE.TXT"] = @"\";
+ }
+ }
+}
+
+#nullable disable
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
new file mode 100644
index 00000000000..9f09b8a2246
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiPayloadPackageProject.wix.cs
@@ -0,0 +1,74 @@
+// 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 System.Xml.Linq;
+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;
+ }
+
+ // 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}";
+ }
+
+ ///
+ 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);
+
+ 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..18610e361c3
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs
@@ -0,0 +1,19 @@
+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 __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..07d9b193c90
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Build.Framework;
+using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
+
+namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
+{
+ ///
+ /// Represents a workload manifest MSI.
+ ///
+ internal class WorkloadManifestMsi : MsiBase
+ {
+ 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(MsiMetadata.Create(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);
+ }
+
+ 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;
+
+ if (WorkloadPackGroups.Any())
+ {
+ jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs");
+
+ string jsonAsString = JsonSerializer.Serialize(WorkloadPackGroups, typeof(IList), new JsonSerializerOptions() { WriteIndented = true });
+ jsonDirectory = Path.Combine(WixSourceDirectory, "json");
+ Directory.CreateDirectory(jsonDirectory);
+
+ string jsonFullPath = Path.GetFullPath(Path.Combine(jsonDirectory, "WorkloadPackGroups.json"));
+ File.WriteAllText(jsonFullPath, 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);
+ }
+
+ NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath);
+ }
+
+ CompilerToolTask candle = CreateDefaultCompiler();
+ candle.AddSourceFiles(packageContentWxs,
+ EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory),
+ EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory),
+ EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory));
+
+ if (jsonContentWxs != null)
+ {
+ candle.AddSourceFiles(jsonContentWxs);
+ candle.AddPreprocessorDefinition("IncludePackGroupJson", "true");
+ candle.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory);
+ }
+ else
+ {
+ candle.AddPreprocessorDefinition("IncludePackGroupJson", "false");
+ }
+
+ // Only extract the include file as it's not compilable, but imported by various source files.
+ EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory);
+
+ // To support upgrades, the UpgradeCode must be stable within an SDK feature band.
+ // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID.
+ // The workload author will need to guarantee that the version for the MSI is higher than previous shipped versions
+ // to ensure upgrades correctly trigger.
+ 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:B}");
+ 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);
+
+ AddDefaultPackageFiles(msi);
+
+ return msi;
+ }
+
+
+ public class WorkloadPackGroupJson
+ {
+ public string? GroupPackageId { get; set; }
+ public string? GroupPackageVersion { get; set; }
+
+ public List Packs { get; set; } = new List();
+ }
+
+ public class WorkloadPackJson
+ {
+ public string? PackId { get; set; }
+
+ public string? PackVersion { get; set; }
+ }
+ }
+}
+
+#nullable disable
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..e8e9d4b982f
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs
@@ -0,0 +1,197 @@
+// 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);
+
+ AddDefaultPackageFiles(msi);
+
+ return msi;
+
+ }
+
+ class MsiDirectory
+ {
+ public string Name { get; }
+ public string Id { get; }
+
+ public Dictionary Subdirectories { get; } = new();
+
+ public MsiDirectory(string name, string id)
+ {
+ Name = name;
+ Id = id;
+ }
+
+ 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
new file mode 100644
index 00000000000..6e4f31a6164
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs
@@ -0,0 +1,103 @@
+// 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(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath)
+ {
+ _package = package;
+ }
+
+ public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null)
+ {
+ // Harvest the package contents before adding it to the source files we need to compile.
+ string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs");
+ string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ?
+ "InstallDir" : "VersionDir";
+
+ 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: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())
+ {
+ 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);
+
+ AddDefaultPackageFiles(msi);
+
+ 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..ae9196db578 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,11 @@
-
-
+
+
+
+
+
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/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/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..28ed9debe7f
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs
@@ -0,0 +1,224 @@
+// 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.
+ /// 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();
+
+ // 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
+
+ if (packGroupId != null)
+ {
+ // 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())
+ {
+ // 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..8421a182126
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/CompilerToolTask.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.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);
+ }
+ }
+
+ public void AddSourceFiles(IEnumerable sourceFiles)
+ {
+ _sourceFiles.AddRange(sourceFiles);
+ }
+
+ ///
+ 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 82%
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..2526f0246d1 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,11 @@ public HeatSuppressions Suppressions
set;
} = HeatSuppressions.SuppressRegistryHarvesting | HeatSuppressions.SuppressRootDirectory;
+ public string SourceVariableName { get; set; } = "SourceDir";
+
+ ///
+ /// The name of the WiX harvest too.
+ ///
protected override string ToolName => "heat.exe";
///
@@ -84,7 +100,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)
{
}
@@ -96,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/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..413c4a0702c
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.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.
+
+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 InstallationRecordKey = nameof(InstallationRecordKey);
+ 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..e2747c4f24c
--- /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), 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/WorkloadPackGroupPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs
new file mode 100644
index 00000000000..d4192a1d38c
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackGroupPackage.wix.cs
@@ -0,0 +1,48 @@
+// 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 = GetPackGroupID(workloadName);
+ }
+
+ public static string GetPackGroupID(string workloadName)
+ {
+ return 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
new file mode 100644
index 00000000000..e8002b061bc
--- /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 sourcePackage, string[] platforms)> 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..96b5640367c
--- /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)}.{Identity.Version}";
+ 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