From 2ffcb2d968684fcb5fa1d725d50fc906aaef9998 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 12 Aug 2025 22:32:35 -0700 Subject: [PATCH 01/26] Update CPM --- Directory.Packages.props | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 13dade5c444..9c2c0c38aa7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -95,8 +95,13 @@ - + + + + + + From 60a4564535f01fbfe18059bbeb45d97bd8d5008e Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 12 Aug 2025 22:36:53 -0700 Subject: [PATCH 02/26] Update DTF references --- .../CreateVisualStudioWorkloadTests.cs | 2 +- ....DotNet.Build.Tasks.Workloads.Tests.csproj | 24 +++++++++---------- .../MsiTests.cs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs index e95fd0db6b2..d3699dbeb79 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -9,7 +9,7 @@ using Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.Deployment.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Xunit; 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 cd322aac72d..3afa3ce0996 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/Microsoft.DotNet.Build.Tasks.Workloads.Tests.csproj @@ -13,6 +13,12 @@ + + + + + + @@ -23,11 +29,13 @@ + + - + @@ -37,12 +45,13 @@ - + - + + @@ -63,13 +72,4 @@ - - - - - - - - - diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 05a2099959d..8ad403a4785 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -8,9 +8,9 @@ using Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.Deployment.WindowsInstaller; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.NET.Sdk.WorkloadManifestReader; +using WixToolset.Dtf.WindowsInstaller; using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests @@ -31,7 +31,7 @@ private static ITaskItem BuildManifestMsi(string path, string msiVersion = "1.2. [WindowsOnlyFact] public void WorkloadManifestsIncludeInstallationRecords() { - ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), + ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), msiOutputPath: Path.Combine(MsiOutputPath, "mrec")); string msiPath603 = msi603.GetMetadata(Metadata.FullPath); From 36db5c1f8a8ade16e05d4f9eae183ed8065bfc8a Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 12 Aug 2025 22:42:40 -0700 Subject: [PATCH 03/26] Update .editorconfig for WiX --- .editorconfig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index b2f8c844227..9576b187900 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,10 @@ tab_width = 2 indent_size = 4 tab_width = 4 +# WiX files +[*.{wixproj,wxs,wxi,wxl,thm}] +indent_size = 2 + # New line preferences csharp_new_line_before_open_brace = all csharp_new_line_before_else = true From ded45f5b83a708f3612412449f358f1628b65f05 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 12 Aug 2025 22:45:44 -0700 Subject: [PATCH 04/26] Remove variables.wxi --- .../src/MsiTemplate/Variables.wxi | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi deleted file mode 100644 index 0e1b15b5322..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - From d1744631bb60c7657873800872a64eb7c7499e04 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 13 Aug 2025 08:48:34 -0700 Subject: [PATCH 05/26] Update packages references for workloads project --- ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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 0f1d87796bc..112a7c29e8d 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 @@ -38,11 +38,11 @@ - - - - - + + + + + @@ -59,18 +59,14 @@ - - + + - + + From 42de590af0c1e197fa6b43980b69b74e9a1530a4 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 13 Aug 2025 08:49:09 -0700 Subject: [PATCH 06/26] Add wixproj project stub --- .../src/MsiTemplate/msi.wixproj | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj new file mode 100644 index 00000000000..8e94a159dca --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + From 7a1e9892c0d2a3121fabcb70fdc220d983f9d295 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 13 Aug 2025 08:50:01 -0700 Subject: [PATCH 07/26] Fix using statements --- .../src/Msi/DirectoryRow.wix.cs | 2 +- .../src/Msi/FileRow.wix.cs | 2 +- .../src/Msi/MsiUtils.wix.cs | 4 ++-- .../src/Msi/RegistryRow.wix.cs | 2 +- .../src/Msi/RelatedProduct.wix.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs index 057f660adaa..86d0896a649 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/DirectoryRow.wix.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Deployment.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs index 30fac6f1b1f..d3701d29328 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/FileRow.wix.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Deployment.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs index 4e7d29f738b..ea438662569 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Deployment.WindowsInstaller; -using Microsoft.Deployment.WindowsInstaller.Package; +using WixToolset.Dtf.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller.Package; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs index e258c2ebabf..6c26459acee 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RegistryRow.wix.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Deployment.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs index 3619e87e3d1..17df42b566c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/RelatedProduct.wix.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Deployment.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { From 35259fadee36178cf48f720391f5db837f5b0805 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 13 Aug 2025 08:50:30 -0700 Subject: [PATCH 08/26] Add .wixproj to embedded templates --- .../src/EmbeddedTemplates.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index 61c5f9f553c..830b1a9d16c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -74,7 +74,7 @@ static EmbeddedTemplates() { "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" }, { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, - { "Variables.wxi", $"{ns}.MsiTemplate.Variables.wxi" }, + { "msi.wixproj", $"{ns}.MsiTemplate.msi.wixproj" }, { $"msi.swr", $"{ns}.SwixTemplate.msi.swr" }, { $"msi.swixproj", $"{ns}.SwixTemplate.msi.swixproj" }, From 228317604f55d04ef9591cb34a4633573a891257 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 27 Aug 2025 23:56:22 -0700 Subject: [PATCH 09/26] Add WiX Project --- ....DotNet.Build.Tasks.Workloads.Tests.csproj | 2 + .../MsiTests.cs | 11 +- .../TestBase.cs | 26 +++ .../WixProjectTests.cs | 94 +++++++++ .../src/Wix/WixProject.cs | 190 ++++++++++++++++++ 5 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs 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 3afa3ce0996..ad0f652404e 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 @@ -31,6 +31,7 @@ + @@ -52,6 +53,7 @@ + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 8ad403a4785..4340c41f3a1 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -135,11 +135,20 @@ public void ItCanBuildATemplatePackMsi() // Template packs should pull in the raw nupkg. We can verify by query the File table. There should // only be a single file. FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault(); - Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupk", fileRow.FileName); + Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupkg", fileRow.FileName); // 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 ItCreatesSetupProects() + { + var msi = CreateWorkloadManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), + msiVersion: "1.2.3"); + + msi.CreateProject(); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs index 52fb73d78bf..b45c4daab66 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -3,6 +3,9 @@ using System; using System.IO; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Microsoft.Arcade.Test.Common; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -17,5 +20,28 @@ public abstract class TestBase public static readonly string WixToolsetPath = Path.Combine(TestAssetsPath, "wix"); public static readonly string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); + + public static readonly string TestOutputRoot = Path.Combine(AppContext.BaseDirectory, "TEST_OUTPUT"); + + /// + /// Returns a new, random directory for test projects. + /// + public string TestProjectDirectory => Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + + internal static WorkloadManifestPackage CreateWorkloadManifestPackage(string packageFile, string msiVersion) + { + string path = Path.Combine(TestAssetsPath, packageFile); + TaskItem packageItem = new(path); + return new(packageItem, PackageRootDirectory, new Version(msiVersion)); + } + + internal static WorkloadManifestMsi CreateWorkloadManifestMsi(string packageFile, string msiVersion, string platform = "x64", string msiOutputPath = null, + bool isSxS = true) + { + WorkloadManifestPackage pkg = CreateWorkloadManifestPackage(packageFile, msiVersion); + WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath, + isSxS: true); + return msi; + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs new file mode 100644 index 00000000000..2d10f6d85f4 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.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. + +using System; +using System.IO; +using Microsoft.DotNet.Build.Tasks.Workloads.Wix; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + public class WixProjectTests : TestBase + { + [WindowsOnlyFact] + public void ItGeneratesAnSdkStyleProject() + { + var wixproj = new WixProject("5.0.2"); + string projectDir = TestProjectDirectory; + string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); + Directory.CreateDirectory(projectDir); + + wixproj.Save(wixProjPath); + + string projectContents = File.ReadAllText(wixProjPath); + + Assert.StartsWith(@"$(DefineConstants);Foo= Bar x64", projectContents,StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs new file mode 100644 index 00000000000..09c5217f3b8 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// Record to track HarvestDirectory item metadata consumed by Heat when + /// + /// The directory to harvest. + /// The name of the component group to create for generated authoring. + /// The ID of the directory reference to use instead of TARGETDIR. + /// The preprocessor variable to use instead of SourceDir. + /// Suppress generation of registry elements. + /// Suppress generation of a Directory element for the parent directory of the file. + public record HarvestDirectoryInfo(string Path, string ComponentGroupName, string DirectoryRefId, string PreprocessorVariable, + bool SuppressRegistry, bool SuppressRootDirectory); + + /// + /// Represents an SDK style WiX project. + /// + public class WixProject + { + private const string _attributeComponentGroupName = "ComponentGroupName"; + private const string _attributeDirectoryRefId = "DirectoryRefId"; + private const string _attributeInclude = "Include"; + private const string _attributePreprocessorVariable = "PreprocessorVariable"; + private const string _attributeSdk = "Sdk"; + private const string _attributeSuppressRegistry = "SuppressRegistry"; + private const string _attributeSuppressRootDirectory = "SuppressRootDirectory"; + private const string _attributeVersion = "Version"; + private const string _elementPropertyGroup = "PropertyGroup"; + private const string _elementProject = "Project"; + private const string _elementItemGroup = "ItemGroup"; + private const string _itemHarvestDirectory = "HarvestDirectory"; + private const string _itemPackageReference = "PackageReference"; + private const string _propertyDefineConstants = "DefineConstants"; + + private const string _defaultSdk = "Microsoft.WixToolset.Sdk"; + + private Dictionary _packageReferences = new(StringComparer.OrdinalIgnoreCase); + + // Preprocessor variables are case sensitive. + private Dictionary _preprocessorDefinitions = new(); + + private Dictionary _properties = new(StringComparer.OrdinalIgnoreCase); + + private Dictionary _harvestDirectories = new(StringComparer.OrdinalIgnoreCase); + + private string _sdk; + + private string _toolsetVersion; + + /// + /// Creates a new instance. + /// + /// The version of the WiX toolset the project will reference. + /// The SDK to use. + public WixProject(string toolsetVersion, string sdk = _defaultSdk) + { + _toolsetVersion = toolsetVersion; + _sdk = sdk; + } + + public void Save(string path) + { + XmlDocument doc = new XmlDocument(); + var project = doc.CreateElement(_elementProject); + + project.SetAttribute(_attributeSdk, $"{_sdk}/{_toolsetVersion}"); + + if (_properties.Count > 0) + { + var propertyGroup = doc.CreateElement(_elementPropertyGroup); + + foreach (var propertyName in _properties.Keys) + { + var property = doc.CreateElement(propertyName); + property.InnerText = _properties[propertyName]; + propertyGroup.AppendChild(property); + } + + project.AppendChild(propertyGroup); + } + + if (_packageReferences.Count > 0) + { + var packageReferencesItemGroup = doc.CreateElement(_elementItemGroup); + + foreach (string packageId in _packageReferences.Keys) + { + var item = doc.CreateElement(_itemPackageReference); + item.SetAttribute(_attributeInclude, packageId); + item.SetAttribute(_attributeVersion, _packageReferences[packageId]); + packageReferencesItemGroup.AppendChild(item); + } + + project.AppendChild(packageReferencesItemGroup); + } + + if (_preprocessorDefinitions.Count > 0) + { + var preprocessorPropertyGroup = doc.CreateElement(_elementPropertyGroup); + + foreach (string key in _preprocessorDefinitions.Keys) + { + var defineConstantsProperty = doc.CreateElement(_propertyDefineConstants); + defineConstantsProperty.InnerText = $"$({_propertyDefineConstants});{key}={_preprocessorDefinitions[key]}"; + preprocessorPropertyGroup.AppendChild(defineConstantsProperty); + } + + project.AppendChild(preprocessorPropertyGroup); + } + + if (_harvestDirectories.Count > 0) + { + var _harvestDirectoryItemGroup = doc.CreateElement(_elementItemGroup); + + foreach (var harvestInfo in _harvestDirectories.Values) + { + var item = doc.CreateElement(_itemHarvestDirectory); + item.SetAttribute(_attributeInclude, harvestInfo.Path); + item.SetAttribute(_attributeComponentGroupName, harvestInfo.ComponentGroupName); + item.SetAttribute(_attributeDirectoryRefId, harvestInfo.DirectoryRefId); + item.SetAttribute(_attributePreprocessorVariable, harvestInfo.PreprocessorVariable); + item.SetAttribute(_attributeSuppressRegistry, harvestInfo.SuppressRegistry.ToString().ToLowerInvariant()); + item.SetAttribute(_attributeSuppressRootDirectory, harvestInfo.SuppressRootDirectory.ToString().ToLowerInvariant()); + + _harvestDirectoryItemGroup.AppendChild(item); + } + + project.AppendChild(_harvestDirectoryItemGroup); + } + + // Add the root Project node. + doc.AppendChild(project); + + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + + using StreamWriter streamWriter = new(path); + using XmlWriter writer = XmlWriter.Create(streamWriter, settings); + doc.Save(writer); + } + + /// + /// Adds a package reference using the specified package identifier and version. + /// + /// The package identifier to add. + /// The version of the package. + public void AddPackageReference(string id, string version) => + _packageReferences[id] = version; + + /// + /// Adds a package reference using the specified package identifier and implicit toolset version. + /// + /// The package identifier to add. + public void AddPackageReference(string id) => + AddPackageReference(id, _toolsetVersion); + + /// + /// Adds a preprocessor definition using the DefineConstants property. + /// + public void AddPreprocessorDefinition(string name, string value) => + _preprocessorDefinitions[name] = value; + + /// + /// Adds an msbuild property. + /// + /// The name of the property to set. + /// The value of the property to set. + public void AddProperty(string name, string value) => + _properties[name] = value; + + /// + /// Adds a directory for harvesting. + /// + public void AddHarvestDirectory(string path, string componentGroupName, string directoryRefId, + string preprocessorVariable, bool suppressRegistry = true, bool suppressRootDirectory = true) => + _harvestDirectories[path] = new HarvestDirectoryInfo(path, componentGroupName, directoryRefId, + preprocessorVariable, suppressRegistry, suppressRootDirectory); + } +} From 3a754e869fcdc0fec2074bfccc104b0ba04aeeb7 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 2 Sep 2025 13:13:25 -0700 Subject: [PATCH 10/26] Update manifest MSI --- ....DotNet.Build.Tasks.Workloads.Tests.csproj | 4 +- .../MsiTests.cs | 28 ++++-- .../TestBase.cs | 4 +- .../WixProjectTests.cs | 21 +++-- .../src/DefaultValues.cs | 5 + ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 21 ++++- .../src/Msi/MsiBase.wix.cs | 54 +++++++++++ .../src/Msi/WorkloadManifestMsi.wix.cs | 93 +++++++++++++++++++ .../src/MsiTemplate/DependencyProvider.wxs | 10 +- .../src/MsiTemplate/Directories.wxs | 19 ++-- .../src/MsiTemplate/ManifestProduct.wxs | 44 ++------- .../src/MsiTemplate/Product.wxs | 3 +- .../src/MsiTemplate/Registry.wxs | 8 +- .../src/MsiTemplate/msi.wixproj | 20 ++-- .../src/ToolsetPackages.cs | 31 +++++++ .../src/Wix/PreprocessorDefinitionNames.cs | 8 ++ .../src/Wix/WixProject.cs | 27 +++++- 17 files changed, 308 insertions(+), 92 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs 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 ad0f652404e..5484b5e3298 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 @@ -48,7 +48,7 @@ - + @@ -73,5 +73,5 @@ - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 4340c41f3a1..04ad9377cd3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -77,6 +77,23 @@ public void ItCanBuildSideBySideManifestMsis() Assert.NotNull(msi604.GetMetadata(Metadata.WixObj)); } + [WindowsOnlyFact] + public void ItCanGenerateAManifestWixProject() + { + string testCaseDirectory = TestCaseDirectory; + testCaseDirectory = @"D:\workloads\manifests\A"; + // Directory where the package will be extracted. + string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); + TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + WorkloadManifestPackage pkg = new(packageItem, packageContentsDirectory, new Version("1.2.3")); + pkg.Extract(); + WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, testCaseDirectory); + + string wixProjPath = msi.CreateProject(ToolsetInfo.MicrosoftWixToolsetVersion); + + string wixProjContents = File.ReadAllText(wixProjPath); + } + [WindowsOnlyFact] public void ItCanBuildAManifestMsi() { @@ -140,15 +157,6 @@ public void ItCanBuildATemplatePackMsi() // 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 ItCreatesSetupProects() - { - var msi = CreateWorkloadManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), - msiVersion: "1.2.3"); - - msi.CreateProject(); - } + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs index b45c4daab66..0edc4765428 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -24,9 +24,9 @@ public abstract class TestBase public static readonly string TestOutputRoot = Path.Combine(AppContext.BaseDirectory, "TEST_OUTPUT"); /// - /// Returns a new, random directory for test projects. + /// Returns a new, random directory for a test case. /// - public string TestProjectDirectory => Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + public string TestCaseDirectory => Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); internal static WorkloadManifestPackage CreateWorkloadManifestPackage(string packageFile, string msiVersion) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs index 2d10f6d85f4..88cda8f8d8a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs @@ -14,7 +14,7 @@ public class WixProjectTests : TestBase public void ItGeneratesAnSdkStyleProject() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestProjectDirectory; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -29,7 +29,7 @@ public void ItGeneratesAnSdkStyleProject() public void PackageReferencesCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestProjectDirectory; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -48,7 +48,7 @@ public void PackageReferencesCanBeAdded() public void PreprocessorDefinitionsCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestProjectDirectory; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -64,7 +64,7 @@ public void PreprocessorDefinitionsCanBeAdded() public void PropertiesCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestProjectDirectory; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -80,15 +80,24 @@ public void PropertiesCanBeAdded() public void HarvestDirectoriesCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestProjectDirectory; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); - wixproj.AddHarvestDirectory(@"x\y\z", "CG_Test", "SOMEDIR", "MyVar"); + wixproj.AddHarvestDirectory(@"x\y\z", "SOMEDIR", "MyVar", "CG_Test"); + // This will select the default component group. + wixproj.AddHarvestDirectory(@"a\b\c", "SOMEDIR2", "MyVar2"); + // Omit the preprocessor variable override. + wixproj.AddHarvestDirectory(@"aa\bb\cc", "SOMEDIR3"); + // Omit the preprocessor variable override. + wixproj.AddHarvestDirectory(@"xx\yy\zz"); wixproj.Save(wixProjPath); string projectContents = File.ReadAllText(wixProjPath); Assert.Contains(@"", projectContents,StringComparison.OrdinalIgnoreCase); + Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase); + Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase); + Assert.Contains(@"", projectContents, StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index 7fd2709c466..99522a1302c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -8,6 +8,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads /// internal static class DefaultValues { + /// + /// Default component group identifier used when harvesting a directory. + /// + public const string DefaultComponentGroupName = "CG_PackageContents"; + /// /// Prefix used in Visual Studio for SWIX based package group. /// 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 112a7c29e8d..a9212ee401e 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 @@ - + $(NetToolCurrent);$(NetFrameworkToolCurrent) @@ -57,6 +57,7 @@ + @@ -69,4 +70,22 @@ + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 0e5c1df333c..bbdd688bbfa 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -158,6 +158,60 @@ protected string GenerateEula() return eulaRtf; } + /// + /// Creates an empty WiX project using the specific toolset version and initialize common properties. + /// + /// The WiX toolset version to use for building the project. + /// An empty project. + /// + /// + /// The following properties are set: InstallerPlatform + /// + /// + /// The following preprocessor variables are included: InstallerVersion + /// + /// + protected WixProject CreateEmptyProject(string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion) + { + if (Directory.Exists(WixSourceDirectory)) + { + Directory.Delete(WixSourceDirectory, true); + } + + Directory.CreateDirectory(WixSourceDirectory); + + WixProject wixproj = new(toolsetVersion); + + // Initialize common properties and preprocessor definitions. + wixproj.AddProperty("InstallerPlatform", Platform); + // Turn off ICE validation. CodeIntegrity and AppLocker block ICE checks that require elevation, even + // when running as administator. + wixproj.AddProperty("SuppressValidation", "true"); + + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Bitness, Platform == "x86" ? "always32" : "always64"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); + // v5.0 was releases with W2K8 R2 and Windows 7. It's also required to support + // arm64. See https://learn.microsoft.com/en-us/windows/win32/msi/released-versions-of-windows-installer + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallerVersion, "500"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Metadata.Id); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}"); + //wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductLanguage, "1033"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform)); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}"); + + // All MSIs must support reference counting. + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetDependencyExtension); + // Util extension is required to access the QueryNativeMachine custom action. + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUtilExtension); + // All workload MSIs (manifests or packs) needs to override the default dialog set and select a minimal UI. + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUIExtension); + + return wixproj; + } + /// /// Creates a new compiler tool task and configures some common extensions and preprocessor /// variables. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 5fc3a15b9b1..f9f2a90809a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -42,6 +42,99 @@ public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBu IsSxS = isSxS; } + public string CreateProject(string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion) + { + WixProject wixproj = CreateEmptyProject(toolsetVersion); + + // Add source files + EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + + // Add package references for WiX + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat); + + // Configure harvesting of the manifest package contents. + string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); + string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data"); + wixproj.AddHarvestDirectory(packageDataDirectory, + IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + PreprocessorDefinitionNames.SourceDir); + + 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; + + // Default the variable to false. If we harvested workload pack group data, we'll override it + wixproj.AddPreprocessorDefinition("IncludePackGroupJson", "false"); + + if (WorkloadPackGroups.Any()) + { + jsonContentWxs = Path.Combine(WixSourceDirectory, "JsonContent.wxs"); + + string jsonAsString = JsonSerializer.Serialize(WorkloadPackGroups, typeof(IList), new JsonSerializerOptions() { WriteIndented = true }); + jsonDirectory = Path.Combine(WixSourceDirectory, "json"); + Directory.CreateDirectory(jsonDirectory); + + string jsonFullPath = Path.GetFullPath(Path.Combine(jsonDirectory, "WorkloadPackGroups.json")); + File.WriteAllText(jsonFullPath, jsonAsString); + + wixproj.AddHarvestDirectory(jsonDirectory, + IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + "JsonSourceDir", + "CG_PackGroupJson"); + + wixproj.AddPreprocessorDefinition("IncludePackGroupJson", "true"); + wixproj.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory); + + NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); + } + + // To support upgrades, the UpgradeCode must be stable within an SDK feature band. + // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. + // The workload author must ensure that the ProductVersion is higher than previously shipped versions. + // For SxS installs the UpgradeCode can be a random GUID. + Guid upgradeCode = IsSxS ? Guid.NewGuid() : + Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}"); + string providerKeyName = IsSxS ? + $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" : + $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}"; + + // Add preprocessor definitions + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledManifests"); + + // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. + // We have to do the same to ensure the keypath generation produces stable GUIDs. + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}"); + + if (IsSxS) + { + wixproj.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version); + } + + // Finally, generate the .wixproj file. Always overwrite an existing content. + if (File.Exists(wixProjectPath)) + { + File.Delete(wixProjectPath); + } + + wixproj.Save(wixProjectPath); + + return wixProjectPath; + } + /// /// public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs index 82010d2552b..516ad70f933 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DependencyProvider.wxs @@ -1,12 +1,10 @@ - - - + - - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs index 09ddad7ddf6..1da8faf0690 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -1,19 +1,20 @@ - - - + + + - - - - - + + + - + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs index c20410c0c6f..eb1ccc8074a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -1,13 +1,12 @@ - - - - + + - + - + @@ -19,31 +18,8 @@ - - NOT WIX_DOWNGRADE_DETECTED - - - - - - - - - - - - - - - - - - - - - - - + @@ -60,6 +36,6 @@ - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs index 43b36f3ce2c..f8a72bd6501 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs @@ -1,6 +1,5 @@ - - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs index 30bb5ead5b8..69643b2c82b 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs @@ -1,11 +1,9 @@ - - - + - - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj index 8e94a159dca..27c5318cd3c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj @@ -1,15 +1,15 @@ - + - - - - + + + $(DefineConstants);Bitness={bitness} + $(DefineConstants);DependencyProviderKeyName={dependencyProviderKeyName} + $(DefineConstants);InstallerVersion={installerVersion} + $(DefineConstants);ProductLanguage={productLanguage} + $(DefineConstants);ProductCode={productCode} + $(DefineConstants);UpgradeCode={upgradeCode} + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs new file mode 100644 index 00000000000..7cb5c661cad --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetPackages.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + /// + /// Defines well-known package identifiers for WiX toolset packages + /// + public class ToolsetPackages + { + /// + /// Provides access to Heat tool for harvesting directories, files, etc. + /// + public const string MicrosoftWixToolsetHeat = "Microsoft.WixToolset.Heat"; + + /// + /// Provides access to the Util extension, including built-in custom actions. + /// + public const string MicrosoftWixToolsetUtilExtension = "Microsoft.WixToolset.Util.wixext"; + + /// + /// Provides access to UI extensions like custom dialog sets for MSIs. + /// + public const string MicrosoftWixToolsetUIExtension = "Microsoft.WixToolset.UI.wixext"; + + /// + /// Provides the dependency provider extension to manage shared installations and MSI reference counting. + /// + public const string MicrosoftWixToolsetDependencyExtension = "Microsoft.WixToolset.Dependency.wixext"; + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs index 363dea4c64f..317dc021281 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs @@ -8,10 +8,17 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix /// public static class PreprocessorDefinitionNames { + public static readonly string Bitness = nameof(Bitness); public static readonly string DependencyProviderKeyName = nameof(DependencyProviderKeyName); public static readonly string EulaRtf = nameof(EulaRtf); public static readonly string InstallDir = nameof(InstallDir); public static readonly string InstallationRecordKey = nameof(InstallationRecordKey); + + /// + /// Specifies the Windows Installer version. + /// + public static readonly string InstallerVersion = nameof(InstallerVersion); + public static readonly string ManifestId = nameof(ManifestId); public static readonly string Manufacturer = nameof(Manufacturer); public static readonly string PackKind = nameof(PackKind); @@ -20,6 +27,7 @@ public static class PreprocessorDefinitionNames public static readonly string Platform = nameof(Platform); public static readonly string ProductCode = nameof(ProductCode); public static readonly string ProductName = nameof(ProductName); + public static readonly string ProductLanguage = nameof(ProductLanguage); public static readonly string ProductVersion = nameof(ProductVersion); public static readonly string SdkFeatureBandVersion = nameof(SdkFeatureBandVersion); public static readonly string SourceDir = nameof(SourceDir); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs index 09c5217f3b8..2dc48a40f57 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs @@ -125,8 +125,17 @@ public void Save(string path) var item = doc.CreateElement(_itemHarvestDirectory); item.SetAttribute(_attributeInclude, harvestInfo.Path); item.SetAttribute(_attributeComponentGroupName, harvestInfo.ComponentGroupName); - item.SetAttribute(_attributeDirectoryRefId, harvestInfo.DirectoryRefId); - item.SetAttribute(_attributePreprocessorVariable, harvestInfo.PreprocessorVariable); + + if (!string.IsNullOrWhiteSpace(harvestInfo.DirectoryRefId)) + { + item.SetAttribute(_attributeDirectoryRefId, harvestInfo.DirectoryRefId); + } + + if (!string.IsNullOrWhiteSpace(harvestInfo.PreprocessorVariable)) + { + item.SetAttribute(_attributePreprocessorVariable, harvestInfo.PreprocessorVariable); + } + item.SetAttribute(_attributeSuppressRegistry, harvestInfo.SuppressRegistry.ToString().ToLowerInvariant()); item.SetAttribute(_attributeSuppressRootDirectory, harvestInfo.SuppressRootDirectory.ToString().ToLowerInvariant()); @@ -145,6 +154,8 @@ public void Save(string path) OmitXmlDeclaration = true }; + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); + using StreamWriter streamWriter = new(path); using XmlWriter writer = XmlWriter.Create(streamWriter, settings); doc.Save(writer); @@ -180,10 +191,16 @@ public void AddProperty(string name, string value) => _properties[name] = value; /// - /// Adds a directory for harvesting. + /// Adds a directory for harvesting. /// - public void AddHarvestDirectory(string path, string componentGroupName, string directoryRefId, - string preprocessorVariable, bool suppressRegistry = true, bool suppressRootDirectory = true) => + /// The local path of the directory to harvest. + /// The ID of the directory to reference instead of TARGETDIR. + /// The preprocessor variable to use instead of SourceDir. + /// The name of the component group to create for generated authoring. + /// Suppress generation of registry elements. + /// Suppress generation of a Directory element for the parent directory of the file. + public void AddHarvestDirectory(string path, string directoryRefId = null, string preprocessorVariable = null, + string componentGroupName = DefaultValues.DefaultComponentGroupName, bool suppressRegistry = true, bool suppressRootDirectory = true) => _harvestDirectories[path] = new HarvestDirectoryInfo(path, componentGroupName, directoryRefId, preprocessorVariable, suppressRegistry, suppressRootDirectory); } From ede0f6e0dfa95763f685c17bc72eb5a00e9c174a Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Thu, 4 Sep 2025 12:56:03 -0700 Subject: [PATCH 11/26] Rewrite some tests for manifests/pack MSIs --- .../MsiTests.cs | 91 +++++++++--- .../TestBase.cs | 9 +- .../WixProjectTests.cs | 20 ++- .../src/EmbeddedTemplates.cs | 3 +- ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 16 +-- .../src/Msi/MsiBase.wix.cs | 129 +++++++++++++++--- .../src/Msi/MsiUtils.wix.cs | 4 +- .../src/Msi/WorkloadManifestMsi.wix.cs | 31 ++--- .../src/Msi/WorkloadPackMsi.wix.cs | 38 +++++- .../MsiTemplate/Directory.Build.targets.pp | 26 ++++ .../src/MsiTemplate/Product.wxs | 14 +- .../src/MsiTemplate/Registry.wxs | 12 +- .../src/Wix/WixProject.cs | 25 +++- .../src/Wix/WixProperties.cs | 37 +++++ .../src/WorkloadPackageBase.cs | 3 +- 15 files changed, 362 insertions(+), 96 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 04ad9377cd3..1ed084db2eb 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -15,17 +15,17 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { - [Collection("6.0.200 Toolchain manifest tests")] + [Collection("MSI tests")] public class MsiTests : TestBase { - private static ITaskItem BuildManifestMsi(string path, string msiVersion = "1.2.3", string platform = "x64", string msiOutputPath = null) + private static ITaskItem BuildManifestMsi(string packagePath, string msiVersion = "1.2.3", string platform = "x64", string msiOutputPath = null) { - TaskItem packageItem = new(path); + TaskItem packageItem = new(packagePath); WorkloadManifestPackage pkg = new(packageItem, PackageRootDirectory, new Version(msiVersion)); pkg.Extract(); WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath, - isSxS: true); - return string.IsNullOrWhiteSpace(msiOutputPath) ? msi.Build(MsiOutputPath) : msi.Build(msiOutputPath); + isSxS: true, overridePackageVersions: true); + return string.IsNullOrWhiteSpace(msiOutputPath) ? msi.Build2(MsiOutputPath) : msi.Build2(msiOutputPath); } [WindowsOnlyFact] @@ -36,8 +36,35 @@ public void WorkloadManifestsIncludeInstallationRecords() string msiPath603 = msi603.GetMetadata(Metadata.FullPath); MsiUtils.GetAllRegistryKeys(msiPath603).Should().Contain(r => - r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3" - ); + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"); + } + + [WindowsOnlyFact] + public void ItCanBuildWorkloadSdkPackMsi() + { + string testCaseDirectory = TestCaseDirectory; + string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); + string msiOutputDirectory = Path.Combine(testCaseDirectory, "msi"); + + TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")); + WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, new Version("1.2.3")); + // Parse the manifest to extract information related to workload packs so we can extract a specific pack. + WorkloadManifest manifest = manifestPackage.GetManifest(); + WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Sdk"); + WorkloadPack pack = manifest.Packs[packId]; + + var sourcePackages = WorkloadPackPackage.GetSourcePackages(TestAssetsPath, pack); + var sourcePackageInfo = sourcePackages.FirstOrDefault(); + var workloadPackPackage = WorkloadPackPackage.Create(pack, sourcePackageInfo.sourcePackage, sourcePackageInfo.platforms, packageContentsDirectory, null, null); + workloadPackPackage.Extract(); + var workloadPackMsi = new WorkloadPackMsi(workloadPackPackage, "x64", new MockBuildEngine(), + WixToolsetPath, testCaseDirectory, overridePackageVersions: true); + + // Build the MSI and verify its contents + var msi = workloadPackMsi.Build2(msiOutputDirectory); + + MsiUtils.GetAllRegistryKeys(msi.GetMetadata(Metadata.FullPath)).Should().Contain(r => + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Sdk.win-x64\6.0.4"); } [WindowsOnlyFact] @@ -71,27 +98,49 @@ public void ItCanBuildSideBySideManifestMsis() d.DirectoryParent == "ManifestIdDir" && d.DefaultDir.EndsWith("|6.0.4")); + // TODO: Need to ensure that we can generate the new wixpacks in build since + // wixobj no longer exists. // Generated MSI should return the path where the .wixobj files are located so - // WiX packs can be created for post-build signing. - Assert.NotNull(msi603.GetMetadata(Metadata.WixObj)); - Assert.NotNull(msi604.GetMetadata(Metadata.WixObj)); + //// WiX packs can be created for post-build signing. + //Assert.NotNull(msi603.GetMetadata(Metadata.WixObj)); + //Assert.NotNull(msi604.GetMetadata(Metadata.WixObj)); } [WindowsOnlyFact] public void ItCanGenerateAManifestWixProject() { - string testCaseDirectory = TestCaseDirectory; - testCaseDirectory = @"D:\workloads\manifests\A"; - // Directory where the package will be extracted. - string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); - TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); - WorkloadManifestPackage pkg = new(packageItem, packageContentsDirectory, new Version("1.2.3")); - pkg.Extract(); - WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, testCaseDirectory); + //string testCaseDirectory = TestCaseDirectory; + //testCaseDirectory = @"D:\workloads\manifests\A"; + //// Directory where the package will be extracted. + //string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); + //TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + //WorkloadManifestPackage pkg = new(packageItem, packageContentsDirectory, new Version("1.2.3")); + //pkg.Extract(); + //WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, testCaseDirectory); + + //string wixProjPath = msi.CreateProject(ToolsetInfo.MicrosoftWixToolsetVersion); + + //var s = new ProcessStartInfo() { UseShellExecute = false }; + //s.EnvironmentVariables["DOTNET_ROOT"] = @"D:\git\forks\Arcade\.dotnet"; + //s.FileName = "dotnet.exe"; + //s.Arguments = $"build {wixProjPath}"; - string wixProjPath = msi.CreateProject(ToolsetInfo.MicrosoftWixToolsetVersion); - string wixProjContents = File.ReadAllText(wixProjPath); + + + + //var p = Process.Start(s); + //p.WaitForExit(); + + string msiPath = @"D:\workloads\manifests\A\src\wix\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3\x64\bin\Debug\dfd3ba2050cfed9781b439ec8b49a823-x64.msi"; + using SummaryInfo si = new(msiPath, enableWrite: false); + + 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); + + //string wixProjContents = File.ReadAllText(wixProjPath); } [WindowsOnlyFact] @@ -157,6 +206,6 @@ public void ItCanBuildATemplatePackMsi() // 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/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs index 0edc4765428..8524ad98e5f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -11,10 +11,17 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { public abstract class TestBase { + /// + /// This is a version of Arcade that contains updated tasks for creating WiX packs that support + /// signing MSIs built using WiX v5. + /// + public static readonly string MicrosoftDotNetBuildTasksInstallersPackageVersion = "10.0.0-beta.25420.109"; + public static readonly string BaseIntermediateOutputPath = Path.Combine(AppContext.BaseDirectory, "obj", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); public static readonly string BaseOutputPath = Path.Combine(AppContext.BaseDirectory, "bin", Path.GetFileNameWithoutExtension(Path.GetTempFileName())); public static readonly string MsiOutputPath = Path.Combine(BaseOutputPath, "msi"); + public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); public static readonly string WixToolsetPath = Path.Combine(TestAssetsPath, "wix"); @@ -26,7 +33,7 @@ public abstract class TestBase /// /// Returns a new, random directory for a test case. /// - public string TestCaseDirectory => Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); + public string TestCaseDirectory => Path.Combine(TestOutputRoot, Path.GetRandomFileName()); internal static WorkloadManifestPackage CreateWorkloadManifestPackage(string packageFile, string msiVersion) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs index 88cda8f8d8a..079ba7f570b 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs @@ -25,23 +25,29 @@ public void ItGeneratesAnSdkStyleProject() Assert.StartsWith(@"")] + [InlineData("Microsoft.WixToolset.Heat", "5.0.3", false, @"")] + [InlineData("Microsoft.WixToolset.Heat", "5.0.3", true, @"")] + public void PackageReferencesCanBeAdded(string packageId, string packageVersion, bool overridePackageVersions, + string expectedPackageReference) { - var wixproj = new WixProject("5.0.2"); + var wixproj = new WixProject("5.0.2") + { + OverridePackageVersions = overridePackageVersions + }; + string projectDir = TestCaseDirectory; string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); - wixproj.AddPackageReference("Microsoft.WixToolset.Heat"); - wixproj.AddPackageReference("Microsoft.WixToolset.Util.wixext", "5.0.3"); + wixproj.AddPackageReference(packageId, packageVersion); wixproj.Save(wixProjPath); string projectContents = File.ReadAllText(wixProjPath); Assert.Contains("Microsoft.WixToolset.Sdk/5.0.2", projectContents); - Assert.Contains(@"PackageReference Include=""Microsoft.WixToolset.Heat"" Version=""5.0.2""", projectContents); - Assert.Contains(@"PackageReference Include=""Microsoft.WixToolset.Util.wixext"" Version=""5.0.3""", projectContents); + Assert.Contains(expectedPackageReference, projectContents); } [WindowsOnlyFact] diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index 830b1a9d16c..2beb959dfd0 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -75,6 +75,7 @@ static EmbeddedTemplates() { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, { "msi.wixproj", $"{ns}.MsiTemplate.msi.wixproj" }, + { "Directory.Build.targets", $"{ns}.MsiTemplate.Directory.Build.targets.pp" }, { $"msi.swr", $"{ns}.SwixTemplate.msi.swr" }, { $"msi.swixproj", $"{ns}.SwixTemplate.msi.swixproj" }, @@ -83,7 +84,7 @@ static EmbeddedTemplates() { $"component.swixproj", $"{ns}.SwixTemplate.component.swixproj" }, { $"manifest.vsmanproj", $"{ns}.SwixTempalte.manifest.vsmanproj" }, { $"packageGroup.swr", $"{ns}.SwixTemplate.packageGroup.swr" }, - { $"packageGroup.swixproj", $"{ns}.SwixTemplate.packageGroup.swixproj" }, + { $"packageGroup.swixproj", $"{ns}.SwixTemplate.packageGroup.swixproj" }, { "Icon.png", $"{ns}.Misc.Icon.png" }, { "LICENSE.TXT", $"{ns}.Misc.LICENSE.TXT" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj index a9212ee401e..dc45a720ed1 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 @@ -57,6 +57,7 @@ + @@ -66,24 +67,13 @@ + - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index bbdd688bbfa..6fd7f199878 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Cryptography; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -74,6 +76,16 @@ protected string BaseIntermediateOutputPath get; } + /// + /// When , package references in the generated .wixproj do not include + /// version information. + /// + public bool ManagePackageVersionsCentrally + { + get; + set; + } + /// /// Gets the value to use for the manufacturer. /// @@ -112,23 +124,51 @@ protected string WixToolsetPath } /// - /// Set of files to include in the NuGet package that will wrap the MSI. Keys represent the source files and the - /// value contains the relative path inside the generated NuGet package. + /// Generate VersionOverride attributes for package references. + /// + protected bool OverridePackageVersions + { + get; + } + + /// + /// The WiX toolset version. This version applies to both the WiX SDK and any additional toolset + /// package references. + /// + protected string WixToolsetVersion + { + get; + } + + /// + /// The package version to use when adding package references to the .wixproj. Returns + /// if is . + /// + protected string? WixToolsetPackageVersion => + ManagePackageVersionsCentrally ? null : WixToolsetVersion; + + /// + /// Set of files to include in the NuGet package that will wrap the MSI. Keys represent source files and + /// values contain relative paths inside the generated NuGet package. /// public Dictionary NuGetPackageFiles { get; set; } = new(); public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath, - string platform, string baseIntermediateOutputPath) + string platform, string baseIntermediateOutputPath, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false) { BuildEngine = buildEngine; WixToolsetPath = wixToolsetPath; Platform = platform; BaseIntermediateOutputPath = baseIntermediateOutputPath; + WixToolsetVersion = toolsetVersion; // 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); +// WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", metadata.Id, $"{metadata.PackageVersion}", platform); + WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", Path.GetRandomFileName()); Metadata = metadata; + OverridePackageVersions = overridePackageVersions; } /// @@ -159,19 +199,21 @@ protected string GenerateEula() } /// - /// Creates an empty WiX project using the specific toolset version and initialize common properties. + /// Creates a basic WiX project using the specific toolset version and sets common properties and + /// package references /// /// The WiX toolset version to use for building the project. /// An empty project. /// /// - /// The following properties are set: InstallerPlatform + /// The following properties are set: InstallerPlatform, SuppressValidation, OutputType, TargetName, + /// DebugType /// /// /// The following preprocessor variables are included: InstallerVersion /// /// - protected WixProject CreateEmptyProject(string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion) + protected virtual WixProject CreateProject() { if (Directory.Exists(WixSourceDirectory)) { @@ -180,17 +222,27 @@ protected WixProject CreateEmptyProject(string toolsetVersion = ToolsetInfo.Micr Directory.CreateDirectory(WixSourceDirectory); - WixProject wixproj = new(toolsetVersion); + WixProject wixproj = new(WixToolsetVersion) { OverridePackageVersions = this.OverridePackageVersions }; + // *********************************************************** // Initialize common properties and preprocessor definitions. - wixproj.AddProperty("InstallerPlatform", Platform); + // *********************************************************** + wixproj.AddProperty(WixProperties.InstallerPlatform, Platform); + // Pacakge is the default in v5, but defaults can change. + wixproj.AddProperty(WixProperties.OutputType, "Package"); // Turn off ICE validation. CodeIntegrity and AppLocker block ICE checks that require elevation, even // when running as administator. - wixproj.AddProperty("SuppressValidation", "true"); + wixproj.AddProperty(WixProperties.SuppressValidation, "true"); + // The WiX SDK will determine the extension based on the output type, e.g. Package -> .msi, Patch -> .msp, etc. + wixproj.AddProperty(WixProperties.TargetName, Path.GetFileNameWithoutExtension(OutputName)); + // WiX only supports "full". If the property is overridden (Directory.build.props), + // the compiler will report a warning, e.g. "warning WIX1098: The value 'embedded' is not a valid value for command line argument '-pdbType'. Using the value 'full' instead." + wixproj.AddProperty(WixProperties.DebugType, "full"); + wixproj.AddProperty("IntermediateOutputPath", @"obj\\$(Configuration)"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Bitness, Platform == "x86" ? "always32" : "always64"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); - // v5.0 was releases with W2K8 R2 and Windows 7. It's also required to support + // v5.0 was released with W2K8 R2 and Windows 7. It's also required to support // arm64. See https://learn.microsoft.com/en-us/windows/win32/msi/released-versions-of-windows-installer wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallerVersion, "500"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer); @@ -202,12 +254,13 @@ protected WixProject CreateEmptyProject(string toolsetVersion = ToolsetInfo.Micr wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform)); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}"); - // All MSIs must support reference counting. - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetDependencyExtension); + // All workload MSIs must support reference counting since they are shared between multiple + // SDKs and Visual Studio. + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetDependencyExtension, WixToolsetPackageVersion); // Util extension is required to access the QueryNativeMachine custom action. - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUtilExtension); - // All workload MSIs (manifests or packs) needs to override the default dialog set and select a minimal UI. - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUIExtension); + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUtilExtension, WixToolsetPackageVersion); + // All workload MSIs (manifests or packs) need to override the default dialog set and select a minimal UI. + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUIExtension, WixToolsetPackageVersion); return wixproj; } @@ -292,6 +345,50 @@ protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem return msiItem; } + /// + /// + /// + /// + /// + public virtual ITaskItem Build2(string outputPath) + { + string wixProjectPath = Path.Combine(WixSourceDirectory, "msi.wixproj"); + WixProject wixproj = CreateProject(); + wixproj.AddProperty("OutputPath", outputPath); + + if (File.Exists(wixProjectPath)) + { + File.Delete(wixProjectPath); + } + + wixproj.Save(wixProjectPath); + + // If DOTNET_HOST_PATH is set, we'll use that. If not, fall back + // to resolivng the local runtime Arcade is using. + string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (string.IsNullOrWhiteSpace(dotnetHostPath)) + { + dotnetHostPath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), @"..\..\..\dotnet.exe"); + } + + ProcessStartInfo startInfo = new() + { + FileName = dotnetHostPath, + Arguments = $"build {wixProjectPath}", + }; + + var buildProcess = Process.Start(startInfo); + buildProcess?.WaitForExit(); + + // Return a task item that contains all the information about the generated MSI. + TaskItem msiItem = new TaskItem(Path.Combine(outputPath, OutputName)); + msiItem.SetMetadata(Workloads.Metadata.Platform, Platform); + 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"; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs index ea438662569..a54bf9928b9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs @@ -27,7 +27,7 @@ public class MsiUtils /// /// Query string to retrieve the dependency provider key from the WixDependencyProvider table. /// - private const string _getWixDependencyProviderQuery = "SELECT `ProviderKey` FROM `WixDependencyProvider`"; + private const string _getWixDependencyProviderQuery = "SELECT `ProviderKey` FROM `Wix4DependencyProvider`"; /// /// Query string to retrieve all the rows from the MSI Directory table. @@ -139,7 +139,7 @@ public static string GetProviderKeyName(string packagePath) using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); - if (db.Tables.Contains("WixDependencyProvider")) + if (db.Tables.Contains("Wix4DependencyProvider")) { using View depProviderView = db.OpenView(_getWixDependencyProviderQuery); depProviderView.Execute(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index f9f2a90809a..df61944d7c3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi @@ -35,27 +36,31 @@ protected bool IsSxS } public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediateOutputPath, bool isSxS = false) : - base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath) + string baseIntermediateOutputPath, bool isSxS = false, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false) : + base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath, toolsetVersion, + overridePackageVersions) { Package = package; IsSxS = isSxS; } - public string CreateProject(string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion) + /// + /// Creates a new WiX project for building a workload manifest installer (MSI). + /// + protected override WixProject CreateProject() { - WixProject wixproj = CreateEmptyProject(toolsetVersion); + WixProject wixproj = base.CreateProject(); // Add source files EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); // Add package references for WiX - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat); + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, WixToolsetPackageVersion); // Configure harvesting of the manifest package contents. string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); @@ -69,7 +74,7 @@ public string CreateProject(string toolsetVersion = ToolsetInfo.MicrosoftWixTool NuGetPackageFiles[file] = @"\data\extractedManifest\" + Path.GetFileName(file); } - // Add WorkloadPackGroups.json to add to workload manifest MSI + // Add WorkloadPackGroups.json to add to workload manifest MSI string? jsonContentWxs = null; string? jsonDirectory = null; @@ -124,16 +129,8 @@ public string CreateProject(string toolsetVersion = ToolsetInfo.MicrosoftWixTool wixproj.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version); } - // Finally, generate the .wixproj file. Always overwrite an existing content. - if (File.Exists(wixProjectPath)) - { - File.Delete(wixProjectPath); - } - - wixproj.Save(wixProjectPath); - - return wixProjectPath; - } + return wixproj; + } /// /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index d98ae2809ca..b4cb9bca960 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -19,12 +19,46 @@ internal class WorkloadPackMsi : MsiBase protected override string BaseOutputName => _package.ShortName; public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediatOutputPath) : - base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + string baseIntermediatOutputPath, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false) : + base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath, toolsetVersion, + overridePackageVersions) { _package = package; } + protected override WixProject CreateProject() + { + WixProject wixproj = base.CreateProject(); + string wixProjectPath = Path.Combine(WixSourceDirectory, "pack.wixproj"); + + EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + + wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, WixToolsetPackageVersion); + + string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? + "InstallDir" : "VersionDir"; + + wixproj.AddHarvestDirectory(_package.DestinationDirectory, directoryReference, + PreprocessorDefinitionNames.SourceDir); + + Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); + string providerKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}"; + + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPacks"); + + return wixproj; + } + public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions = null) { // Harvest the package contents before adding it to the source files we need to compile. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp new file mode 100644 index 00000000000..0b393a92a51 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp @@ -0,0 +1,26 @@ + + + + + $(IntermediateOutputPath)wixpack + $(ArtifactsNonShippingPackagesDir) + + + + + + + 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 f8a72bd6501..50a366eb694 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs @@ -1,10 +1,8 @@ - - - - - + + @@ -20,6 +18,6 @@ - - + + 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 69643b2c82b..a8bbfc9974c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs @@ -4,12 +4,12 @@ - - - - - - + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs index 2dc48a40f57..49c1109b14c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs @@ -33,6 +33,7 @@ public class WixProject private const string _attributeSuppressRegistry = "SuppressRegistry"; private const string _attributeSuppressRootDirectory = "SuppressRootDirectory"; private const string _attributeVersion = "Version"; + private const string _attributeVersionOverride = "VersionOverride"; private const string _elementPropertyGroup = "PropertyGroup"; private const string _elementProject = "Project"; private const string _elementItemGroup = "ItemGroup"; @@ -55,6 +56,15 @@ public class WixProject private string _toolsetVersion; + /// + /// Replace version attributes, + /// + public bool OverridePackageVersions + { + get; + set; + } + /// /// Creates a new instance. /// @@ -95,7 +105,20 @@ public void Save(string path) { var item = doc.CreateElement(_itemPackageReference); item.SetAttribute(_attributeInclude, packageId); - item.SetAttribute(_attributeVersion, _packageReferences[packageId]); + + // Allow null/empty versions in case CPM already defined the packages. + if (!string.IsNullOrEmpty(_packageReferences[packageId])) + { + if (OverridePackageVersions) + { + item.SetAttribute(_attributeVersionOverride, _packageReferences[packageId]); + } + else + { + item.SetAttribute(_attributeVersion, _packageReferences[packageId]); + } + } + packageReferencesItemGroup.AppendChild(item); } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs new file mode 100644 index 00000000000..a464d1260bb --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix +{ + /// + /// Property names used inside a WiX project (.wixproj). + /// + internal class WixProperties + { + /// + /// The platform of the installer being built. + /// + public static readonly string InstallerPlatform = nameof(InstallerPlatform); + + /// + /// Turns off validation (ICE) when set to true. + /// + public static readonly string SuppressValidation = nameof(SuppressValidation); + + /// + /// The name of the output produced by the .wixproj. The extension is determined by the WiX SDK + /// based on the output type. + /// + public static readonly string TargetName = nameof(TargetName); + + /// + /// The type of output produced by the project, for example, Package produces an MSI, Patch produces an MSP, etc. + /// + public static readonly string OutputType = nameof(OutputType); + + /// + /// The debug information to emit. + /// + public static readonly string DebugType = nameof(DebugType); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs index 08213015640..ad461432f73 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs @@ -200,7 +200,8 @@ public WorkloadPackageBase(string packagePath, string destinationBaseDirectory, Title = nuspec.GetTitle(); PackagePath = packagePath; - DestinationDirectory = Path.Combine(destinationBaseDirectory, $"{Identity}"); + + DestinationDirectory = Path.Combine(destinationBaseDirectory, Path.GetRandomFileName()); ShortNames = shortNames; PackageFileName = Path.GetFileNameWithoutExtension(packagePath); From a5a90dc1d4d78122eb5e5c93f03498bddf907931 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Mon, 15 Sep 2025 21:09:20 -0700 Subject: [PATCH 12/26] Fix unit tests, disable pack groups --- .../CreateVisualStudioWorkloadSetTests.cs | 11 +- .../MsiTests.cs | 120 ++++---- .../SwixPackageTests.cs | 2 +- .../TestBase.cs | 19 +- .../WixProjectTests.cs | 10 +- .../src/CreateVisualStudioWorkload.wix.cs | 8 +- .../src/CreateVisualStudioWorkloadSet.wix.cs | 2 +- .../src/Msi/MsiBase.wix.cs | 217 ++++++-------- .../src/Msi/WorkloadManifestMsi.wix.cs | 189 +++---------- .../src/Msi/WorkloadPackGroupMsi.wix.cs | 264 ++++++++++-------- .../src/Msi/WorkloadPackMsi.wix.cs | 84 +----- .../src/Msi/WorkloadSetMsi.wix.cs | 73 ++--- .../src/MsiTemplate/Product.wxs | 6 +- .../src/MsiTemplate/WorkloadSetProduct.wxs | 40 ++- .../src/VisualStudioWorkloadTaskBase.wix.cs | 18 +- .../src/Wix/WixProject.cs | 29 +- .../src/WorkloadPackageBase.cs | 5 + 17 files changed, 440 insertions(+), 657 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs index 670aa8a5659..83670699168 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -26,13 +26,13 @@ public static void ItCanCreateWorkloadSets() Directory.Delete(baseIntermediateOutputPath, recursive: true); } - ITaskItem[] workloadSetPackages = new[] - { + ITaskItem[] workloadSetPackages = + [ new TaskItem(Path.Combine(TestAssetsPath, "microsoft.net.workloads.9.0.100.9.0.100-baseline.1.23464.1.nupkg")) .WithMetadata(Metadata.MsiVersion, "12.8.45") - }; + ]; - IBuildEngine buildEngine = new MockBuildEngine(); + var buildEngine = new MockBuildEngine(); CreateVisualStudioWorkloadSet createWorkloadSetTask = new CreateVisualStudioWorkloadSet() { @@ -43,7 +43,8 @@ public static void ItCanCreateWorkloadSets() WorkloadSetPackageFiles = workloadSetPackages }; - Assert.True(createWorkloadSetTask.Execute()); + Assert.True(createWorkloadSetTask.Execute(), buildEngine.BuildErrorEvents.Count > 0 ? + buildEngine.BuildErrorEvents[0].Message : "Task failed. No error events"); // Spot check the x64 generated MSI. ITaskItem msi = createWorkloadSetTask.Msis.Where(i => i.GetMetadata(Metadata.Platform) == "x64").FirstOrDefault(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 1ed084db2eb..b005aee4c70 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -18,21 +18,31 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests [Collection("MSI tests")] public class MsiTests : TestBase { - private static ITaskItem BuildManifestMsi(string packagePath, string msiVersion = "1.2.3", string platform = "x64", string msiOutputPath = null) + /// + /// Helper method for generating workload manifest MSIs. + /// + /// The directory to use for generated output (WiX project, MSI, etc.) + /// The path of the workload manifest NuGet package. + /// The ProductVersion to assign to the MSI. + /// The platform of the MSI. + /// Whether MSIs should allow side-by-side installations instead of major upgrades. + /// A task item with metadata for the generated MSI. + private static ITaskItem BuildManifestMsi(string outputDirectory, string packagePath, string msiVersion = "1.2.3", string platform = "x64", + bool allowSideBySideInstalls = true) { TaskItem packageItem = new(packagePath); - WorkloadManifestPackage pkg = new(packageItem, PackageRootDirectory, new Version(msiVersion)); + WorkloadManifestPackage pkg = new(packageItem, Path.Combine(outputDirectory, "pkg"), new Version(msiVersion)); pkg.Extract(); - WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath, - isSxS: true, overridePackageVersions: true); - return string.IsNullOrWhiteSpace(msiOutputPath) ? msi.Build2(MsiOutputPath) : msi.Build2(msiOutputPath); + WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), outputDirectory, + allowSideBySideInstalls, overridePackageVersions: true); + + return msi.Build(Path.Combine(outputDirectory, "msi")); } [WindowsOnlyFact] public void WorkloadManifestsIncludeInstallationRecords() { - ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), - msiOutputPath: Path.Combine(MsiOutputPath, "mrec")); + ITaskItem msi603 = BuildManifestMsi(GetTestCaseDirectory(), Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); string msiPath603 = msi603.GetMetadata(Metadata.FullPath); MsiUtils.GetAllRegistryKeys(msiPath603).Should().Contain(r => @@ -42,7 +52,7 @@ public void WorkloadManifestsIncludeInstallationRecords() [WindowsOnlyFact] public void ItCanBuildWorkloadSdkPackMsi() { - string testCaseDirectory = TestCaseDirectory; + string testCaseDirectory = GetTestCaseDirectory(); string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); string msiOutputDirectory = Path.Combine(testCaseDirectory, "msi"); @@ -61,23 +71,32 @@ public void ItCanBuildWorkloadSdkPackMsi() WixToolsetPath, testCaseDirectory, overridePackageVersions: true); // Build the MSI and verify its contents - var msi = workloadPackMsi.Build2(msiOutputDirectory); + var msi = workloadPackMsi.Build(msiOutputDirectory); + string msiPath = msi.GetMetadata(Metadata.FullPath); - MsiUtils.GetAllRegistryKeys(msi.GetMetadata(Metadata.FullPath)).Should().Contain(r => + // Verify workload record + MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Sdk.win-x64\6.0.4"); + + // Process the summary information stream's template to extract the MSIs target platform. + using SummaryInfo si = new(msiPath, enableWrite: false); + Assert.Equal("x64;1033", si.Template); + + // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). + Assert.Equal("{A06E6854-C6B0-3C8D-8D0C-F0704755303B}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); } [WindowsOnlyFact] public void ItCanBuildSideBySideManifestMsis() { - string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, "pkg"); + string outputDirectory = GetTestCaseDirectory(); // Build 6.0.200 manifest for version 6.0.3 - ITaskItem msi603 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + ITaskItem msi603 = BuildManifestMsi(outputDirectory, Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); string msiPath603 = msi603.GetMetadata(Metadata.FullPath); // Build 6.0.200 manifest for version 6.0.4 - ITaskItem msi604 = BuildManifestMsi(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.4.nupkg")); + ITaskItem msi604 = BuildManifestMsi(outputDirectory, Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.4.nupkg")); string msiPath604 = msi604.GetMetadata(Metadata.FullPath); // For upgradable MSIs, the 6.0.4 and 6.0.3 copies of the package would have generated the same @@ -104,57 +123,29 @@ public void ItCanBuildSideBySideManifestMsis() //// WiX packs can be created for post-build signing. //Assert.NotNull(msi603.GetMetadata(Metadata.WixObj)); //Assert.NotNull(msi604.GetMetadata(Metadata.WixObj)); - } + } [WindowsOnlyFact] - public void ItCanGenerateAManifestWixProject() + public void ItCanBuildAManifestMsi() { - //string testCaseDirectory = TestCaseDirectory; - //testCaseDirectory = @"D:\workloads\manifests\A"; - //// Directory where the package will be extracted. - //string packageContentsDirectory = Path.Combine(testCaseDirectory, "pkg"); - //TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); - //WorkloadManifestPackage pkg = new(packageItem, packageContentsDirectory, new Version("1.2.3")); - //pkg.Extract(); - //WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, testCaseDirectory); - - //string wixProjPath = msi.CreateProject(ToolsetInfo.MicrosoftWixToolsetVersion); - - //var s = new ProcessStartInfo() { UseShellExecute = false }; - //s.EnvironmentVariables["DOTNET_ROOT"] = @"D:\git\forks\Arcade\.dotnet"; - //s.FileName = "dotnet.exe"; - //s.Arguments = $"build {wixProjPath}"; - + string outputDirectory = GetTestCaseDirectory(); + //string pkgDirectory = Path.Combine(outputDirectory, "pkg"); + //string msiDirectory = Path.Combine(outputDirectory, "msi"); + ITaskItem msi = BuildManifestMsi(outputDirectory, + Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), + allowSideBySideInstalls: false); - //var p = Process.Start(s); - //p.WaitForExit(); - - string msiPath = @"D:\workloads\manifests\A\src\wix\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3\x64\bin\Debug\dfd3ba2050cfed9781b439ec8b49a823-x64.msi"; - using SummaryInfo si = new(msiPath, enableWrite: false); - - 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); - - //string wixProjContents = File.ReadAllText(wixProjPath); - } - - [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); + //TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); + //WorkloadManifestPackage pkg = new(packageItem, pkgDirectory, new Version("1.2.3")); + //pkg.Extract(); + //WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), outputDirectory); - ITaskItem item = msi.Build(MsiOutputPath); + //ITaskItem item = msi.Build(msiDirectory); - string msiPath = item.GetMetadata(Metadata.FullPath); + string msiPath = msi.GetMetadata(Metadata.FullPath); // Process the summary information stream's template to extract the MSIs target platform. using SummaryInfo si = new(msiPath, enableWrite: false); @@ -170,21 +161,22 @@ public void ItCanBuildAManifestMsi() // 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)); + //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); + string outputDirectory = GetTestCaseDirectory(); + string pkgDirectory = Path.Combine(outputDirectory, "pkg"); + string msiDirectory = Path.Combine(outputDirectory, "msi"); + WorkloadPack templatePack = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null); + TemplatePackPackage pkg = new(templatePack, packagePath, new[] { "x64" }, pkgDirectory); pkg.Extract(); var buildEngine = new MockBuildEngine(); - WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem item = msi.Build(MsiOutputPath); + WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetPath, outputDirectory, overridePackageVersions: true); + ITaskItem item = msi.Build(msiDirectory); string msiPath = item.GetMetadata(Metadata.FullPath); @@ -202,10 +194,6 @@ public void ItCanBuildATemplatePackMsi() // only be a single file. FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault(); Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupkg", 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/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs index 99b98e1b112..955047d7ca7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { - [Collection("SWIX Package")] + [Collection("SWIX Package Generation")] public class SwixPackageTests : TestBase { [WindowsOnlyFact] diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs index 8524ad98e5f..241e3a30fef 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -33,22 +33,7 @@ public abstract class TestBase /// /// Returns a new, random directory for a test case. /// - public string TestCaseDirectory => Path.Combine(TestOutputRoot, Path.GetRandomFileName()); - - internal static WorkloadManifestPackage CreateWorkloadManifestPackage(string packageFile, string msiVersion) - { - string path = Path.Combine(TestAssetsPath, packageFile); - TaskItem packageItem = new(path); - return new(packageItem, PackageRootDirectory, new Version(msiVersion)); - } - - internal static WorkloadManifestMsi CreateWorkloadManifestMsi(string packageFile, string msiVersion, string platform = "x64", string msiOutputPath = null, - bool isSxS = true) - { - WorkloadManifestPackage pkg = CreateWorkloadManifestPackage(packageFile, msiVersion); - WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath, - isSxS: true); - return msi; - } + public string GetTestCaseDirectory() => + Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs index 079ba7f570b..64f556cd535 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/WixProjectTests.cs @@ -14,7 +14,7 @@ public class WixProjectTests : TestBase public void ItGeneratesAnSdkStyleProject() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestCaseDirectory; + string projectDir = GetTestCaseDirectory(); string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -37,7 +37,7 @@ public void PackageReferencesCanBeAdded(string packageId, string packageVersion, OverridePackageVersions = overridePackageVersions }; - string projectDir = TestCaseDirectory; + string projectDir = GetTestCaseDirectory(); string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -54,7 +54,7 @@ public void PackageReferencesCanBeAdded(string packageId, string packageVersion, public void PreprocessorDefinitionsCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestCaseDirectory; + string projectDir = GetTestCaseDirectory(); string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -70,7 +70,7 @@ public void PreprocessorDefinitionsCanBeAdded() public void PropertiesCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestCaseDirectory; + string projectDir = GetTestCaseDirectory(); string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); @@ -86,7 +86,7 @@ public void PropertiesCanBeAdded() public void HarvestDirectoriesCanBeAdded() { var wixproj = new WixProject("5.0.2"); - string projectDir = TestCaseDirectory; + string projectDir = GetTestCaseDirectory(); string wixProjPath = Path.Combine(projectDir, "msi.wixproj"); Directory.CreateDirectory(projectDir); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs index 5865ac44ac7..bc42b2ce64d 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -166,7 +166,7 @@ protected override bool ExecuteCore() Dictionary manifestMsisByPlatform = new(); foreach (string platform in SupportedPlatforms) { - var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, EnableSideBySideManifests); + var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, BaseIntermediateOutputPath, EnableSideBySideManifests); manifestMsisToBuild.Add(manifestMsi); manifestMsisByPlatform[platform] = manifestMsi; } @@ -342,7 +342,7 @@ protected override bool ExecuteCore() _ = Parallel.ForEach(data.FeatureBands.Keys, platform => { WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Generate a .csproj to package the MSI and its manifest for CLI installs. MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); @@ -389,7 +389,7 @@ protected override bool ExecuteCore() foreach (var platform in packGroup.ManifestsPerPlatform.Keys) { WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Generate a .csproj to package the MSI and its manifest for CLI installs. MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); @@ -431,7 +431,7 @@ protected override bool ExecuteCore() // Visual Studio. _ = Parallel.ForEach(manifestMsisToBuild, msi => { - ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch if (_supportsMachineArch[msi.Package.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs index 97ea915591e..661e7f1a891 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs @@ -67,7 +67,7 @@ protected override bool ExecuteCore() _ = Parallel.ForEach(workloadSetMsisToBuild, msi => { - ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Generate a .csproj to package the MSI and its manifest for CLI installs. MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 6fd7f199878..e4616d32199 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -16,6 +16,9 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { + /// + /// Base class used for building MSIs. + /// internal abstract class MsiBase { /// @@ -60,14 +63,6 @@ protected IBuildEngine BuildEngine get; } - /// - /// The directory where the compiler output (.wixobj files) will be generated. - /// - protected string CompilerOutputPath - { - get; - } - /// /// The root intermediate output directory. /// @@ -78,9 +73,10 @@ protected string BaseIntermediateOutputPath /// /// When , package references in the generated .wixproj do not include - /// version information. + /// version information. This is for repos that rely on CPM and building other installers using + /// SDK style projects. /// - public bool ManagePackageVersionsCentrally + protected internal bool ManagePackageVersionsCentrally { get; set; @@ -95,7 +91,7 @@ public bool ManagePackageVersionsCentrally DefaultValues.Manufacturer; /// - /// The platform of the MSI. + /// The platform (bitness) of the MSI. /// protected string Platform { @@ -116,28 +112,31 @@ protected string WixSourceDirectory } /// - /// The directory containing the WiX toolset binaries. + /// Generate VersionOverride attributes for package references. This avoids conflicts when + /// using CPM and a different version of WiX for non-workload related projects in the same repository. /// - protected string WixToolsetPath + protected bool OverridePackageVersions { get; } /// - /// Generate VersionOverride attributes for package references. + /// The WiX toolset version. This version applies to both the WiX SDK and any additional toolset + /// package references for extensions. /// - protected bool OverridePackageVersions + protected string WixToolsetVersion { get; } /// - /// The WiX toolset version. This version applies to both the WiX SDK and any additional toolset - /// package references. + /// When set to , a wixpack archive will be generated when the MSI is compiled. + /// The wixpack is used to sign an MSI and its contents when using Arcade. /// - protected string WixToolsetVersion + protected bool GenerateWixPack { get; + set; } /// @@ -153,31 +152,54 @@ protected string WixToolsetVersion /// public Dictionary NuGetPackageFiles { get; set; } = new(); - public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath, - string platform, string baseIntermediateOutputPath, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false) + /// + /// The MSI UpgradeCode. + /// + protected abstract Guid UpgradeCode + { + get; + } + + /// + /// The provider key name used to manage MSI dependents. + /// + protected abstract string ProviderKeyName + { + get; + } + + /// + /// The name of the registry key for tracking installation records used by the CLI and + /// and finalizer. May be if the MSI does not support installation + /// records. + /// + protected abstract string? InstallationRecordKey + { + get; + } + + /// + /// Creates a new instance of the class. + /// + /// Metadata passed to the task that are used to build the MSI. + /// + /// + /// + /// The version of the WiX toolset to use for building the installer. + /// + public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, + string platform, string baseIntermediateOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false, bool generateWixPack = false) { BuildEngine = buildEngine; - WixToolsetPath = wixToolsetPath; Platform = platform; BaseIntermediateOutputPath = baseIntermediateOutputPath; - WixToolsetVersion = toolsetVersion; - - // 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); + WixToolsetVersion = wixToolsetVersion; WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", Path.GetRandomFileName()); Metadata = metadata; OverridePackageVersions = overridePackageVersions; - } - - /// - /// 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); + GenerateWixPack = generateWixPack; + } /// /// Gets the platform specific ProductName MSI property. @@ -190,6 +212,7 @@ protected string GetProductName(string platform) => /// /// Generates a EULA (RTF file) that contains the license URL of the underlying NuGet package. /// + /// The full path the generated EULA. protected string GenerateEula() { string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf"); @@ -200,7 +223,7 @@ protected string GenerateEula() /// /// Creates a basic WiX project using the specific toolset version and sets common properties and - /// package references + /// package references. /// /// The WiX toolset version to use for building the project. /// An empty project. @@ -242,17 +265,26 @@ protected virtual WixProject CreateProject() wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Bitness, Platform == "x86" ? "always32" : "always64"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); - // v5.0 was released with W2K8 R2 and Windows 7. It's also required to support + // Windows Install 5.0 was released with W2K8 R2 and Windows 7. It's also required to support // arm64. See https://learn.microsoft.com/en-us/windows/win32/msi/released-versions-of-windows-installer wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallerVersion, "500"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Manufacturer, Manufacturer); + // The package ID and version used to generate the MSI is stored as properties, but + // has no effect on the MSI. It's only purpose is to capture some information about + // the source package. wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageId, Metadata.Id); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackageVersion, $"{Metadata.PackageVersion}"); - //wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Platform, Platform); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductCode, $"{Guid.NewGuid():B}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductLanguage, "1033"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductName, GetProductName(Platform)); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ProductVersion, $"{Metadata.MsiVersion}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{UpgradeCode:B}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, ProviderKeyName); + + if (!string.IsNullOrWhiteSpace(InstallationRecordKey)) + { + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, InstallationRecordKey); + } // All workload MSIs must support reference counting since they are shared between multiple // SDKs and Visual Studio. @@ -266,91 +298,11 @@ protected virtual WixProject CreateProject() } /// - /// Creates a new compiler tool task and configures some common extensions and preprocessor - /// variables. + /// Builds the MSI and returns a task item with metadata about the MSI. /// - /// - 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; - } - - /// - /// - /// - /// - /// - public virtual ITaskItem Build2(string outputPath) + /// The path containing the directory where the MSI will be generated. + /// A task item containing metadata related to the MSI. + public virtual ITaskItem Build(string outputPath) { string wixProjectPath = Path.Combine(WixSourceDirectory, "msi.wixproj"); WixProject wixproj = CreateProject(); @@ -363,14 +315,27 @@ public virtual ITaskItem Build2(string outputPath) wixproj.Save(wixProjectPath); - // If DOTNET_HOST_PATH is set, we'll use that. If not, fall back - // to resolivng the local runtime Arcade is using. + if (GenerateWixPack) + { + // Wixpacks need to capture compile time information from the WiX SDK to rebuild the MSI + // after replacing any unsigned content when using Arcade to sign. + + // TODO: Extract and modify the Directory.Build.targets template and replace some tokens. + } + + // Use DOTNET_HOST_PATH if set, otherwise, fall back to resolivng the host relative to + // the runtime being used. string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); if (string.IsNullOrWhiteSpace(dotnetHostPath)) { dotnetHostPath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), @"..\..\..\dotnet.exe"); } + if (!File.Exists(dotnetHostPath)) + { + throw new InvalidOperationException("Unable to find a suitable host."); + } + ProcessStartInfo startInfo = new() { FileName = dotnetHostPath, @@ -386,6 +351,8 @@ public virtual ITaskItem Build2(string outputPath) msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}"); msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId); + AddDefaultPackageFiles(msiItem); + return msiItem; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index df61944d7c3..99cb32946d9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -27,22 +27,52 @@ internal class WorkloadManifestMsi : MsiBase protected override string BaseOutputName => Path.GetFileNameWithoutExtension(Package.PackagePath); /// - /// True if the manifest installer supports side-by-side installs, otherwise the installer - /// supports major upgrades. + /// True if the manifest installer supports side-by-side installs, otherwise it's + /// assumed the installer supports major upgrades. /// - protected bool IsSxS + /// + /// Major upgrades require both the ProductVersion and ProductCode to change. Refer to the + /// Windows Installer for + /// more details + /// + protected bool AllowSideBySideInstalls { get; } - public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediateOutputPath, bool isSxS = false, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false) : - base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediateOutputPath, toolsetVersion, + // To support upgrades, the UpgradeCode must be stable within an SDK feature band. + // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. + // The workload author must ensure that the ProductVersion is higher than previously shipped versions. + // For SxS installs the UpgradeCode can be a random GUID. + protected override Guid UpgradeCode => + AllowSideBySideInstalls ? Guid.NewGuid() : + Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}"); + + protected override string ProviderKeyName => + AllowSideBySideInstalls ? $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" : + $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}"; + + protected override string? InstallationRecordKey => "InstalledManifests"; + + /// + /// + /// + /// The NuGet package containing the workload manifest. + /// The target platform of the installer. + /// + /// + /// Determines whether manifest installers are side-by-side for an SDK feature band or support major upgrades. + /// The version of the WiX toolset to use for building the installer. + /// Determines if VersionOverride attributes are generated for package references. + /// Determines if a wixpack archive should be generated. The wixpack is required to sign MSIs using Arcade. + public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, + string baseIntermediateOutputPath, bool allowSideBySideInstalls = false, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false, bool generateWixPack = false) : + base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediateOutputPath, wixToolsetVersion, overridePackageVersions) { Package = package; - IsSxS = isSxS; + AllowSideBySideInstalls = allowSideBySideInstalls; } /// @@ -59,14 +89,11 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); - // Add package references for WiX - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, WixToolsetPackageVersion); - // Configure harvesting of the manifest package contents. string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data"); wixproj.AddHarvestDirectory(packageDataDirectory, - IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, PreprocessorDefinitionNames.SourceDir); foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f))) @@ -93,7 +120,7 @@ protected override WixProject CreateProject() File.WriteAllText(jsonFullPath, jsonAsString); wixproj.AddHarvestDirectory(jsonDirectory, - IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, "JsonSourceDir", "CG_PackGroupJson"); @@ -103,152 +130,20 @@ protected override WixProject CreateProject() NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); } - // To support upgrades, the UpgradeCode must be stable within an SDK feature band. - // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. - // The workload author must ensure that the ProductVersion is higher than previously shipped versions. - // For SxS installs the UpgradeCode can be a random GUID. - Guid upgradeCode = IsSxS ? Guid.NewGuid() : - Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}"); - string providerKeyName = IsSxS ? - $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" : - $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}"; - - // Add preprocessor definitions - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); + // Add preprocessor definitions wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledManifests"); // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. // We have to do the same to ensure the keypath generation produces stable GUIDs. wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}"); - if (IsSxS) + if (AllowSideBySideInstalls) { wixproj.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version); } return wixproj; - } - - /// - /// - 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 = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.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 = IsSxS ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, - OutputFile = jsonContentWxs, - Platform = this.Platform, - SourceDirectory = jsonDirectory, - SourceVariableName = "JsonSourceDir", - ComponentGroupName = "CG_PackGroupJson" - }; - - if (!jsonHeat.Execute()) - { - throw new Exception(Strings.HeatFailedToHarvest); - } - - NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); - } - - CompilerToolTask candle = CreateDefaultCompiler(); - candle.AddSourceFiles(packageContentWxs, - EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory)); - - if (IsSxS) - { - candle.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version); - } - - if (jsonContentWxs != null) - { - candle.AddSourceFiles(jsonContentWxs); - candle.AddPreprocessorDefinition("IncludePackGroupJson", "true"); - candle.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory); - } - else - { - candle.AddPreprocessorDefinition("IncludePackGroupJson", "false"); - } - - // Only extract the include file as it's not compilable, but imported by various source files. - EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); - - // To support upgrades, the UpgradeCode must be stable within an SDK feature band. - // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform and manifest ID. - // The workload author will need to guarantee that the version for the MSI is higher than previous shipped versions - // to ensure upgrades correctly trigger. For SxS installs we use the package identity that would include that includes - // the package version. - Guid upgradeCode = IsSxS ? Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.Identity};{Platform}") : - Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.ManifestId};{Package.SdkFeatureBand};{Platform}"); - string providerKeyName = IsSxS ? - $"{Package.ManifestId},{Package.SdkFeatureBand},{Package.PackageVersion},{Platform}" : - $"{Package.ManifestId},{Package.SdkFeatureBand},{Platform}"; - - // Set up additional preprocessor definitions. - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledManifests"); - - // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. - // We have to do the same to ensure the keypath generation produces stable GUIDs. - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}"); - - if (!candle.Execute()) - { - throw new Exception(Strings.FailedToCompileMsi); - } - - ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions); - - AddDefaultPackageFiles(msi); - - return msi; } public class WorkloadPackGroupJson diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index d0795dbb3b3..1ea448de310 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using System; using System.Collections.Generic; using System.IO; @@ -19,136 +21,152 @@ internal class WorkloadPackGroupMsi : MsiBase /// protected override string BaseOutputName => Metadata.Id; - public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediatOutputPath) - : base(package.GetMsiMetadata(), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + protected override string ProviderKeyName => + $"{_package.Id},{Metadata.PackageVersion},{Platform}"; + + protected override string? InstallationRecordKey => "InstalledPackGroups"; + + protected override Guid UpgradeCode => + Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}"); + + public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine, + string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false, bool generateWixPack = false) + : base(package.GetMsiMetadata(), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion, + overridePackageVersions, generateWixPack) { _package = package; } - public override ITaskItem Build(string outputPath, ITaskItem[] iceSuppressions) + protected override WixProject CreateProject() { - 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); - } + WixProject wixproj = base.CreateProject(); - 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, OutputName); - - ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); - - AddDefaultPackageFiles(msi); - - return msi; + return wixproj; } +// 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. + + +//// 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, OutputName); + +// ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + +// AddDefaultPackageFiles(msi); + +// return msi; +// } + class MsiDirectory { public string Name { get; } @@ -195,3 +213,5 @@ public XElement ToXml() } } } + +#nullable disable diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index b4cb9bca960..6cf20cd1592 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -18,11 +18,19 @@ internal class WorkloadPackMsi : MsiBase /// protected override string BaseOutputName => _package.ShortName; + protected override string ProviderKeyName => + $"{_package.Id},{_package.PackageVersion},{Platform}"; + + protected override Guid UpgradeCode => + Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); + + protected override string? InstallationRecordKey => "InstalledPacks"; + public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediatOutputPath, string toolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false) : - base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath, toolsetVersion, - overridePackageVersions) + string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false, bool generateWixPack = false) : + base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion, + overridePackageVersions, generateWixPack) { _package = package; } @@ -38,82 +46,18 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); - wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, WixToolsetPackageVersion); - string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? "InstallDir" : "VersionDir"; wixproj.AddHarvestDirectory(_package.DestinationDirectory, directoryReference, - PreprocessorDefinitionNames.SourceDir); - - Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); - string providerKeyName = $"{_package.Id},{_package.PackageVersion},{Platform}"; + PreprocessorDefinitionNames.SourceDir); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledPacks"); return wixproj; - } - - 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); - } - - ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions); - - AddDefaultPackageFiles(msi); - - return msi; - } + } /// /// Get the installation directory based on the kind of workload pack. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs index 28a461c2e26..c9cfff9b28e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -19,63 +19,40 @@ internal class WorkloadSetMsi : MsiBase protected override string BaseOutputName => Path.GetFileNameWithoutExtension(_package.PackagePath); - public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, - string baseIntermediatOutputPath) : - base(MsiMetadata.Create(package), buildEngine, wixToolsetPath, platform, baseIntermediatOutputPath) + protected override string ProviderKeyName => + $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}"; + + protected override string? InstallationRecordKey => "InstalledWorkloadSets"; + + protected override Guid UpgradeCode => + Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); + + public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine, + string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, + bool overridePackageVersions = false, bool generateWixPack = false) : + base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediatOutputPath, + wixToolsetVersion, overridePackageVersions, generateWixPack) { _package = package; } - public override ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions) + protected override WixProject CreateProject() { - // Harvest the package contents before adding it to the source files we need to compile. - string packageContentWxs = Path.Combine(WixSourceDirectory, "PackageContent.wxs"); - string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); - - HarvesterToolTask heat = new(BuildEngine, WixToolsetPath) - { - DirectoryReference = MsiDirectories.WorkloadSetVersionDirectory, - OutputFile = packageContentWxs, - Platform = this.Platform, - SourceDirectory = packageDataDirectory - }; + WixProject wixproj = base.CreateProject(); - if (!heat.Execute()) - { - throw new Exception(Strings.HeatFailedToHarvest); - } + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("WorkloadSetProduct.wxs", WixSourceDirectory); - CompilerToolTask candle = CreateDefaultCompiler(); - candle.AddSourceFiles(packageContentWxs, - EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory), - EmbeddedTemplates.Extract("WorkloadSetProduct.wxs", WixSourceDirectory)); - - // Extract the include file as it's not compilable, but imported by various source files. - EmbeddedTemplates.Extract("Variables.wxi", WixSourceDirectory); - - Guid upgradeCode = Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); - string providerKeyName = $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}"; - - // Set up additional preprocessor definitions. - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeCode, $"{upgradeCode:B}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.DependencyProviderKeyName, $"{providerKeyName}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.WorkloadSetVersion, $"{_package.WorkloadSetVersion}"); - candle.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallationRecordKey, $"InstalledWorkloadSets"); - - if (!candle.Execute()) - { - throw new Exception(Strings.FailedToCompileMsi); - } - - ITaskItem msi = Link(candle.OutputPath, Path.Combine(outputPath, OutputName), iceSuppressions); + string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); + wixproj.AddHarvestDirectory(packageDataDirectory, MsiDirectories.WorkloadSetVersionDirectory); - AddDefaultPackageFiles(msi); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.WorkloadSetVersion, $"{_package.WorkloadSetVersion}"); - return msi; + return wixproj; } } } 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 50a366eb694..a9d42928dde 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs @@ -1,8 +1,8 @@ - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs index 01429f57ff6..0013d2bfe9e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs @@ -1,34 +1,26 @@ - - - - - - - - - - + + - - + + - - - - - - - - - + + + + + + + + @@ -36,6 +28,6 @@ - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs index 4439fc1ed61..147da0d91ad 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs @@ -40,15 +40,6 @@ public string BaseOutputPath set; } - /// - /// A set of Internal Consistency Evaluators (ICEs) to suppress. - /// - public ITaskItem[] IceSuppressions - { - get; - set; - } - /// /// A set of items containing all the MSIs that were generated. Additional metadata /// is provided for the projects that need to be built to produce NuGet packages for @@ -92,6 +83,15 @@ public string WixToolsetPath set; } + /// + /// The version of the WiX toolset to use. + /// + public string WixToolsetVersion + { + get; + set; + } + /// /// Core execution of the build task. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs index 49c1109b14c..10318366678 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs @@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix { /// - /// Record to track HarvestDirectory item metadata consumed by Heat when + /// Record to track HarvestDirectory item metadata consumed by Heat when generating setup authoring. /// /// The directory to harvest. /// The name of the component group to create for generated authoring. @@ -52,12 +52,12 @@ public class WixProject private Dictionary _harvestDirectories = new(StringComparer.OrdinalIgnoreCase); - private string _sdk; + private string _wixToolsetSdk; private string _toolsetVersion; /// - /// Replace version attributes, + /// Generate VersionOverride attributes for package references. /// public bool OverridePackageVersions { @@ -68,20 +68,25 @@ public bool OverridePackageVersions /// /// Creates a new instance. /// - /// The version of the WiX toolset the project will reference. - /// The SDK to use. - public WixProject(string toolsetVersion, string sdk = _defaultSdk) + /// The version of the WiX toolset the project will reference. + /// The version applies to both the toolset SDK and package references. + /// The WiX toolset SDK to use. + public WixProject(string toolsetVersion, string wixToolsetSdk = _defaultSdk) { _toolsetVersion = toolsetVersion; - _sdk = sdk; + _wixToolsetSdk = wixToolsetSdk; } + /// + /// Generates a .wixproj (XML) file using the specified path and current configuration. + /// + /// The path of the .wixproj to generate. public void Save(string path) { XmlDocument doc = new XmlDocument(); var project = doc.CreateElement(_elementProject); - project.SetAttribute(_attributeSdk, $"{_sdk}/{_toolsetVersion}"); + project.SetAttribute(_attributeSdk, $"{_wixToolsetSdk}/{_toolsetVersion}"); if (_properties.Count > 0) { @@ -214,7 +219,8 @@ public void AddProperty(string name, string value) => _properties[name] = value; /// - /// Adds a directory for harvesting. + /// Adds a directory for harvesting. A package reference for Heat (the harvesting tool) will automatically + /// be added to the project. /// /// The local path of the directory to harvest. /// The ID of the directory to reference instead of TARGETDIR. @@ -223,8 +229,11 @@ public void AddProperty(string name, string value) => /// Suppress generation of registry elements. /// Suppress generation of a Directory element for the parent directory of the file. public void AddHarvestDirectory(string path, string directoryRefId = null, string preprocessorVariable = null, - string componentGroupName = DefaultValues.DefaultComponentGroupName, bool suppressRegistry = true, bool suppressRootDirectory = true) => + string componentGroupName = DefaultValues.DefaultComponentGroupName, bool suppressRegistry = true, bool suppressRootDirectory = true) + { _harvestDirectories[path] = new HarvestDirectoryInfo(path, componentGroupName, directoryRefId, preprocessorVariable, suppressRegistry, suppressRootDirectory); + AddPackageReference(ToolsetPackages.MicrosoftWixToolsetHeat, _toolsetVersion); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs index ad461432f73..01c05683907 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackageBase.cs @@ -201,6 +201,11 @@ public WorkloadPackageBase(string packagePath, string destinationBaseDirectory, PackagePath = packagePath; + // Previously this extracted to a directory containing the package identity, but for testing + // inside Arcade under v5, Heat reports errors for workload packs like Emscripten that + // have deep structure. + // heat.exe : error HEAT0001: System.Runtime.InteropServices.COMException (0x00000003): Failed to get short + // path buffer size for file: D:\git\forks\arcade\artifacts\bin\Microsoft.DotNet.Build.Tasks.Workloads.Tests\Debug\net10.0\TEST_OUTPUT\qbdhgr2p.dpg\pkg\Microsoft.NET.Runtime.Emscripten.2.0.23.Sdk.win-x64.6.0.4\tools\emscripten\cache\sysroot\include\c++\v1\__support\win32\limits_msvc_win32.h DestinationDirectory = Path.Combine(destinationBaseDirectory, Path.GetRandomFileName()); ShortNames = shortNames; From 75c9ed05e228432d1945172ba1a470ee2f2db73f Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 16 Sep 2025 23:10:56 -0700 Subject: [PATCH 13/26] Add support for generating wixpacks --- .../MsiTests.cs | 49 +++++++-------- .../src/EmbeddedTemplates.cs | 2 +- .../src/Metadata.cs | 5 ++ ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 14 ++++- .../src/Msi/MsiBase.wix.cs | 61 +++++++++++++------ .../src/Msi/WorkloadManifestMsi.wix.cs | 7 ++- .../src/Msi/WorkloadPackMsi.wix.cs | 6 +- .../src/MsiTemplate/Directories.wxs | 34 ++++------- .../MsiTemplate/Directory.Build.targets.pp | 2 +- .../MsiTemplate/WorkloadPackDirectories.wxs | 15 +++++ .../src/MsiTemplate/msi.wixproj | 20 ------ .../src/Utils.cs | 23 +++++++ 12 files changed, 141 insertions(+), 97 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadPackDirectories.wxs delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index b005aee4c70..41d56d7768d 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -28,13 +28,14 @@ public class MsiTests : TestBase /// Whether MSIs should allow side-by-side installations instead of major upgrades. /// A task item with metadata for the generated MSI. private static ITaskItem BuildManifestMsi(string outputDirectory, string packagePath, string msiVersion = "1.2.3", string platform = "x64", - bool allowSideBySideInstalls = true) + bool allowSideBySideInstalls = true, bool generateWixpack = false, string wixpackOutputDirectory = null) { TaskItem packageItem = new(packagePath); WorkloadManifestPackage pkg = new(packageItem, Path.Combine(outputDirectory, "pkg"), new Version(msiVersion)); pkg.Extract(); WorkloadManifestMsi msi = new(pkg, platform, new MockBuildEngine(), outputDirectory, - allowSideBySideInstalls, overridePackageVersions: true); + allowSideBySideInstalls, overridePackageVersions: true, generateWixpack: generateWixpack, + wixpackOutputDirectory: wixpackOutputDirectory); return msi.Build(Path.Combine(outputDirectory, "msi")); } @@ -82,6 +83,10 @@ public void ItCanBuildWorkloadSdkPackMsi() using SummaryInfo si = new(msiPath, enableWrite: false); Assert.Equal("x64;1033", si.Template); + // Verify pack directories + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("PackageDir", "because it's an SDK pack"); + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); + // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). Assert.Equal("{A06E6854-C6B0-3C8D-8D0C-F0704755303B}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); } @@ -116,52 +121,37 @@ public void ItCanBuildSideBySideManifestMsis() d.Directory == "ManifestVersionDir" && d.DirectoryParent == "ManifestIdDir" && d.DefaultDir.EndsWith("|6.0.4")); - - // TODO: Need to ensure that we can generate the new wixpacks in build since - // wixobj no longer exists. - // Generated MSI should return the path where the .wixobj files are located so - //// WiX packs can be created for post-build signing. - //Assert.NotNull(msi603.GetMetadata(Metadata.WixObj)); - //Assert.NotNull(msi604.GetMetadata(Metadata.WixObj)); - } + } [WindowsOnlyFact] public void ItCanBuildAManifestMsi() { string outputDirectory = GetTestCaseDirectory(); - //string pkgDirectory = Path.Combine(outputDirectory, "pkg"); - //string msiDirectory = Path.Combine(outputDirectory, "msi"); - + string wixpackOutputDirectory = Path.Combine(outputDirectory, "wixpack"); - ITaskItem msi = BuildManifestMsi(outputDirectory, + ITaskItem msi = BuildManifestMsi(outputDirectory, Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg"), - allowSideBySideInstalls: false); - - - //TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); - //WorkloadManifestPackage pkg = new(packageItem, pkgDirectory, new Version("1.2.3")); - //pkg.Extract(); - //WorkloadManifestMsi msi = new(pkg, "x64", new MockBuildEngine(), outputDirectory); - - //ITaskItem item = msi.Build(msiDirectory); + allowSideBySideInstalls: false, + generateWixpack: true, + wixpackOutputDirectory: wixpackOutputDirectory); string msiPath = msi.GetMetadata(Metadata.FullPath); // Process the summary information stream's template to extract the MSIs target platform. using SummaryInfo si = new(msiPath, enableWrite: false); - // UpgradeCode is predictable/stable for manifest MSIs. + // UpgradeCode is predictable/stable for manifest MSIs that support major upgrades. Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); Assert.Equal("1.2.3", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion)); Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", MsiUtils.GetProviderKeyName(msiPath)); Assert.Equal("x64;1033", si.Template); // There should be no version directory present if the old upgrade model is used. - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir"); + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir", + "because the manifest MSI supports major upgrades"); - // 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)); + // Verify that the wixpack archive was created. + Assert.True(File.Exists(msi.GetMetadata(Metadata.Wixpack))); } [WindowsOnlyFact] @@ -194,6 +184,9 @@ public void ItCanBuildATemplatePackMsi() // only be a single file. FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault(); Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupkg", fileRow.FileName); + + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("PackageDir", "because it's a template pack"); + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index 2beb959dfd0..f80c7bbf489 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -69,12 +69,12 @@ static EmbeddedTemplates() { { "DependencyProvider.wxs", $"{ns}.MsiTemplate.DependencyProvider.wxs" }, { "Directories.wxs", $"{ns}.MsiTemplate.Directories.wxs" }, + { "WorkloadPackDirectories.wxs", $"{ns}.MsiTemplate.WorkloadPackDirectories.wxs" }, { "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" }, { "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" }, { "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" }, { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, - { "msi.wixproj", $"{ns}.MsiTemplate.msi.wixproj" }, { "Directory.Build.targets", $"{ns}.MsiTemplate.Directory.Build.targets.pp" }, { $"msi.swr", $"{ns}.SwixTemplate.msi.swr" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs index 229bb00056e..37b159bd01c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs @@ -58,5 +58,10 @@ internal static class Metadata /// the compiler. /// public static readonly string WixObj = nameof(WixObj); + + /// + /// Metadata containing the full path to the generated wixpack archive. + /// + public static readonly string Wixpack = nameof(Wixpack); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj index dc45a720ed1..0e2a9bc3150 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 @@ -67,13 +67,21 @@ - - - + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index e4616d32199..858ba584322 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; +using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; @@ -21,6 +21,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi /// internal abstract class MsiBase { + /// + /// The Arcade package that contains the CreateWixBuildWixpack task to support signing. + /// + private const string _MicrosoftDotNetBuildTaskInstallers = "Microsoft.DotNet.Build.Tasks.Installers"; + /// /// Replacement token for license URLs in the generated EULA. /// @@ -133,7 +138,7 @@ protected string WixToolsetVersion /// When set to , a wixpack archive will be generated when the MSI is compiled. /// The wixpack is used to sign an MSI and its contents when using Arcade. /// - protected bool GenerateWixPack + protected bool GenerateWixpack { get; set; @@ -152,11 +157,20 @@ protected bool GenerateWixPack /// public Dictionary NuGetPackageFiles { get; set; } = new(); + /// + /// The output directory to use when generating a wixpack for signing. + /// + public string? WixpackOutputDirectory + { + get; + init; + } + /// /// The MSI UpgradeCode. /// - protected abstract Guid UpgradeCode - { + protected abstract Guid UpgradeCode + { get; } @@ -187,9 +201,10 @@ protected abstract string? InstallationRecordKey /// /// The version of the WiX toolset to use for building the installer. /// - public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, + public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string platform, string baseIntermediateOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false, bool generateWixPack = false) + bool overridePackageVersions = false, bool generateWixpack = false, + string? wixpackOutputDirectory = null) { BuildEngine = buildEngine; Platform = platform; @@ -198,8 +213,9 @@ public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", Path.GetRandomFileName()); Metadata = metadata; OverridePackageVersions = overridePackageVersions; - GenerateWixPack = generateWixPack; - } + GenerateWixpack = generateWixpack; + WixpackOutputDirectory = wixpackOutputDirectory; + } /// /// Gets the platform specific ProductName MSI property. @@ -308,6 +324,18 @@ public virtual ITaskItem Build(string outputPath) WixProject wixproj = CreateProject(); wixproj.AddProperty("OutputPath", outputPath); + if (GenerateWixpack) + { + // Wixpacks need to capture compile time information from the WiX SDK to rebuild the MSI + // after replacing any unsigned content when using Arcade to sign. + Utils.StringReplace(EmbeddedTemplates.Extract("Directory.Build.targets", WixSourceDirectory), + Encoding.UTF8, ("__WIXPACK_OUTPUT_DIR__", WixpackOutputDirectory)); + + // Add a package reference to pull in the CreateWixBuildWixpack task. The version + // should automatically default to the "major.minor.path-*", e.g. 10.0.0-* + wixproj.AddPackageReference(_MicrosoftDotNetBuildTaskInstallers, ToolsetInfo.ArcadeVersion); + } + if (File.Exists(wixProjectPath)) { File.Delete(wixProjectPath); @@ -315,14 +343,6 @@ public virtual ITaskItem Build(string outputPath) wixproj.Save(wixProjectPath); - if (GenerateWixPack) - { - // Wixpacks need to capture compile time information from the WiX SDK to rebuild the MSI - // after replacing any unsigned content when using Arcade to sign. - - // TODO: Extract and modify the Directory.Build.targets template and replace some tokens. - } - // Use DOTNET_HOST_PATH if set, otherwise, fall back to resolivng the host relative to // the runtime being used. string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); @@ -345,12 +365,19 @@ public virtual ITaskItem Build(string outputPath) var buildProcess = Process.Start(startInfo); buildProcess?.WaitForExit(); - // Return a task item that contains all the information about the generated MSI. + // Return a task item that contains information about the generated MSI. TaskItem msiItem = new TaskItem(Path.Combine(outputPath, OutputName)); msiItem.SetMetadata(Workloads.Metadata.Platform, Platform); msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}"); msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId); + if (GenerateWixpack && !string.IsNullOrEmpty(WixpackOutputDirectory)) + { + msiItem.SetMetadata(Workloads.Metadata.Wixpack, Path.Combine( + WixpackOutputDirectory, + Path.GetFileNameWithoutExtension(OutputName)) + ".msi.wixpack.zip"); + } + AddDefaultPackageFiles(msiItem); return msiItem; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 99cb32946d9..79f8b452149 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -64,12 +64,13 @@ protected bool AllowSideBySideInstalls /// Determines whether manifest installers are side-by-side for an SDK feature band or support major upgrades. /// The version of the WiX toolset to use for building the installer. /// Determines if VersionOverride attributes are generated for package references. - /// Determines if a wixpack archive should be generated. The wixpack is required to sign MSIs using Arcade. + /// Determines if a wixpack archive should be generated. The wixpack is required to sign MSIs using Arcade. + /// The directory to use for generating a wixpack for signing. public WorkloadManifestMsi(WorkloadManifestPackage package, string platform, IBuildEngine buildEngine, string baseIntermediateOutputPath, bool allowSideBySideInstalls = false, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false, bool generateWixPack = false) : + bool overridePackageVersions = false, bool generateWixpack = false, string? wixpackOutputDirectory = null) : base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediateOutputPath, wixToolsetVersion, - overridePackageVersions) + overridePackageVersions, generateWixpack, wixpackOutputDirectory) { Package = package; AllowSideBySideInstalls = allowSideBySideInstalls; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 6cf20cd1592..615f16381af 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -28,9 +28,10 @@ internal class WorkloadPackMsi : MsiBase public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, - bool overridePackageVersions = false, bool generateWixPack = false) : + bool overridePackageVersions = false, bool generateWixPack = false, + string? wixpackOutputDirectory = null) : base(MsiMetadata.Create(package), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion, - overridePackageVersions, generateWixPack) + overridePackageVersions, generateWixPack, wixpackOutputDirectory) { _package = package; } @@ -44,6 +45,7 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("WorkloadPackDirectories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs index 1da8faf0690..06a0014520a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -12,28 +12,13 @@ - - - - - - - - - - - - - - - - + - + @@ -43,7 +28,7 @@ - + @@ -52,12 +37,17 @@ - - - - + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp index 0b393a92a51..15859dd19b6 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp @@ -3,7 +3,7 @@ $(IntermediateOutputPath)wixpack - $(ArtifactsNonShippingPackagesDir) + __WIXPACK_OUTPUT_DIR__ + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj deleted file mode 100644 index 27c5318cd3c..00000000000 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/msi.wixproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - $(DefineConstants);Bitness={bitness} - $(DefineConstants);DependencyProviderKeyName={dependencyProviderKeyName} - $(DefineConstants);InstallerVersion={installerVersion} - $(DefineConstants);ProductLanguage={productLanguage} - $(DefineConstants);ProductCode={productCode} - $(DefineConstants);UpgradeCode={upgradeCode} - - - - - - - - - diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs index b858b4bc4f3..a4d78cc6946 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs @@ -79,6 +79,29 @@ internal static void StringReplace(string fileName, Dictionary t File.SetAttributes(fileName, oldAttributes); } + /// + /// Replaces all the tokens in a file using the provided replacement tokens. Each key-value-pair define the + /// token to replace (key) and its replacement (value). + /// + /// The file to modify. + /// The encoding to use when updating the file. + /// An array of replacement tokens. + internal static void StringReplace(string fileName, Encoding encoding, params (string Key, string Value)[] tokenReplacements) + { + FileAttributes oldAttributes = File.GetAttributes(fileName); + File.SetAttributes(fileName, oldAttributes & ~FileAttributes.ReadOnly); + + string content = File.ReadAllText(fileName); + + foreach (var token in tokenReplacements) + { + content = content.Replace(token.Key, token.Value); + } + + File.WriteAllText(fileName, content, encoding); + File.SetAttributes(fileName, oldAttributes); + } + /// /// Checks whether a string parameter is neither nor empty. /// From cdd79840502a90f9226fb4099d9fe356c8a5501b Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Fri, 19 Sep 2025 00:14:23 -0700 Subject: [PATCH 14/26] Update workload set authoring and tests --- .../CreateVisualStudioWorkloadSetTests.cs | 10 +++++++--- .../src/CreateVisualStudioWorkloadSet.wix.cs | 2 +- .../src/Msi/WorkloadManifestMsi.wix.cs | 4 ++-- .../src/Msi/WorkloadSetMsi.wix.cs | 4 +++- .../src/MsiTemplate/WorkloadSetProduct.wxs | 4 ++-- .../src/VisualStudioWorkloadTaskBase.wix.cs | 10 ++++++++++ .../src/Wix/PreprocessorDefinitionNames.cs | 1 + 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs index 83670699168..f25b655f6c0 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -15,11 +15,12 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests public class CreateVisualStudioWorkloadSetTests : TestBase { [WindowsOnlyFact] - public static void ItCanCreateWorkloadSets() + public void ItCanCreateWorkloadSets() { // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up // conflicting sources from previous runs. - string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLS"); + string testCaseDirectory = GetTestCaseDirectory(); + string baseIntermediateOutputPath = testCaseDirectory; if (Directory.Exists(baseIntermediateOutputPath)) { @@ -36,9 +37,11 @@ public static void ItCanCreateWorkloadSets() CreateVisualStudioWorkloadSet createWorkloadSetTask = new CreateVisualStudioWorkloadSet() { - BaseOutputPath = BaseOutputPath, + BaseOutputPath = Path.Combine(testCaseDirectory, "msi"), BaseIntermediateOutputPath = baseIntermediateOutputPath, BuildEngine = buildEngine, + OverridePackageVersions = true, + WixToolsetPath = WixToolsetPath, WorkloadSetPackageFiles = workloadSetPackages }; @@ -58,6 +61,7 @@ public static void ItCanCreateWorkloadSets() r.Value == "12.8.45"); // Workload sets are SxS. Verify that we don't have an Upgrade table. + // This requires suppressing the default behavior by setting Package@UpgradeStrategy to "none". Assert.False(MsiUtils.HasTable(msi.ItemSpec, "Upgrade")); // Verify the workloadset version directory and only look at the long name version. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs index 661e7f1a891..d05afbf12c7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs @@ -51,7 +51,7 @@ protected override bool ExecuteCore() foreach (string platform in SupportedPlatforms) { var workloadSetMsi = new WorkloadSetMsi(workloadSetPackage, platform, BuildEngine, - WixToolsetPath, BaseIntermediateOutputPath); + BaseIntermediateOutputPath, overridePackageVersions: OverridePackageVersions); workloadSetMsisToBuild.Add(workloadSetMsi); } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 79f8b452149..37289f5bb0e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -55,7 +55,7 @@ protected bool AllowSideBySideInstalls protected override string? InstallationRecordKey => "InstalledManifests"; /// - /// + /// Creates a new instance. /// /// The NuGet package containing the workload manifest. /// The target platform of the installer. @@ -131,7 +131,7 @@ protected override WixProject CreateProject() NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); } - // Add preprocessor definitions + // Add preprocessor definitions wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs index c9cfff9b28e..795a194e95d 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -46,10 +46,12 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("WorkloadSetProduct.wxs", WixSourceDirectory); string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); - wixproj.AddHarvestDirectory(packageDataDirectory, MsiDirectories.WorkloadSetVersionDirectory); + wixproj.AddHarvestDirectory(packageDataDirectory, MsiDirectories.WorkloadSetVersionDirectory, + PreprocessorDefinitionNames.SourceDir); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}"); + wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.UpgradeStrategy, "none"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.WorkloadSetVersion, $"{_package.WorkloadSetVersion}"); return wixproj; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs index 0013d2bfe9e..2a03f814627 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs @@ -1,8 +1,8 @@ - + InstallerVersion="$(InstallerVersion)" Compressed="yes" Scope="perMachine" UpgradeStrategy="$(UpgradeStrategy)"> diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs index 147da0d91ad..49df0e34c44 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs @@ -92,6 +92,16 @@ public string WixToolsetVersion set; } + /// + /// Generate VersionOverride attributes for package references. This avoids conflicts when + /// using CPM and a different version of WiX for non-workload related projects in the same repository. + /// + public bool OverridePackageVersions + { + get; + set; + } + /// /// Core execution of the build task. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs index 317dc021281..3d08fc743c4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/PreprocessorDefinitionNames.cs @@ -32,6 +32,7 @@ public static class PreprocessorDefinitionNames public static readonly string SdkFeatureBandVersion = nameof(SdkFeatureBandVersion); public static readonly string SourceDir = nameof(SourceDir); public static readonly string UpgradeCode = nameof(UpgradeCode); + public static readonly string UpgradeStrategy = nameof(UpgradeStrategy); public static readonly string WorkloadSetVersion = nameof(WorkloadSetVersion); } } From a0af372425b03bd250f073d276f5d2198bf83d12 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 3 Dec 2025 10:15:39 -0800 Subject: [PATCH 15/26] Convert workload pack groups to WiX 5 --- Arcade.slnx | 2 +- .../CreateVisualStudioWorkloadSetTests.cs | 2 +- .../CreateVisualStudioWorkloadTests.cs | 73 ++++ ....DotNet.Build.Tasks.Workloads.Tests.csproj | 74 ++++ .../MsiTests.cs | 65 +++- .../src/CreateVisualStudioWorkload.wix.cs | 3 + .../src/EmbeddedTemplates.cs | 2 + .../src/Msi/MsiBase.wix.cs | 45 ++- .../src/Msi/MsiTokens.cs | 15 + .../src/Msi/PayloadPackageTokens.wix.cs | 4 - .../src/Msi/WorkloadPackGroupMsi.wix.cs | 320 +++++++++++------- .../src/Msi/WorkloadPackMsi.wix.cs | 23 +- .../MsiTemplate/Directory.Build.targets.pp | 9 +- .../src/MsiTemplate/DirectoryReference.wxs | 8 + .../src/MsiTemplate/PackDirectories.wxs | 27 ++ .../src/Wix/WixProperties.cs | 5 + .../src/WorkloadPackPackage.wix.cs | 42 ++- 17 files changed, 581 insertions(+), 138 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs diff --git a/Arcade.slnx b/Arcade.slnx index cc381104afc..a4c51eaafd6 100644 --- a/Arcade.slnx +++ b/Arcade.slnx @@ -45,6 +45,7 @@ + @@ -71,7 +72,6 @@ - diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs index f25b655f6c0..5e0cad83022 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -62,7 +62,7 @@ public void ItCanCreateWorkloadSets() // Workload sets are SxS. Verify that we don't have an Upgrade table. // This requires suppressing the default behavior by setting Package@UpgradeStrategy to "none". - Assert.False(MsiUtils.HasTable(msi.ItemSpec, "Upgrade")); + MsiUtils.HasTable(msi.ItemSpec, "Upgrade").Should().BeFalse("because workload sets are side-by-side"); // Verify the workloadset version directory and only look at the long name version. DirectoryRow versionDir = MsiUtils.GetAllDirectories(msi.ItemSpec).FirstOrDefault(d => string.Equals(d.Directory, "WorkloadSetVersionDir")); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs index d3699dbeb79..efb2dec4df4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -10,14 +10,87 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using WixToolset.Dtf.WindowsInstaller; +using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Xunit; +using FluentAssertions.Equivalency; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { [Collection("Workload Creation")] public class CreateVisualStudioWorkloadTests : TestBase { + [SkipOnCI(reason: "This test creates workload pack groups for WASM. The test requires almost 1GB of packages and takes a few minutes to run.")] + [WindowsOnlyFact] + public static void ItCreatesPackGroups() + { + // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up + // conflicting sources from previous runs. + string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLPG"); + + if (Directory.Exists(baseIntermediateOutputPath)) + { + Directory.Delete(baseIntermediateOutputPath, recursive: true); + } + + ITaskItem[] manifestsPackages = + { + new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")) + .WithMetadata(Metadata.MsiVersion, "10.0.456") + }; + + IBuildEngine buildEngine = new MockBuildEngine(); + CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() + { + AllowMissingPacks = true, + BaseOutputPath = TestBase.BaseOutputPath, + BaseIntermediateOutputPath = baseIntermediateOutputPath, + BuildEngine = buildEngine, + CreateWorkloadPackGroups = true, + ComponentResources = Array.Empty(), + ManifestMsiVersion = null, + PackageSource = TestBase.TestAssetsPath, + ShortNames = Array.Empty(), + WixToolsetPath = TestBase.WixToolsetPath, + WorkloadManifestPackageFiles = manifestsPackages, + IsOutOfSupportInVisualStudio = false + }; + + bool result = createWorkloadTask.Execute(); + Assert.True(result); + + //WorkloadManifestPackage p = new(manifestsPackages[0], Path.Combine(Path.GetTempPath(), "WLPG"), Version.Parse("1.2.3")); + //WorkloadManifest manifest = p.GetManifest(); + + //List packs = new(); + + //foreach (WorkloadDefinition workload in manifest.Workloads.Values) + //{ + // if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null)) + // { + // foreach (WorkloadPackId packId in wd.Packs) + // { + // packs.Add(manifest.Packs[packId]); + // } + // } + //} + + + + //List l = new List(); + + //foreach (var p2 in packs) + //{ + // foreach (var x in WorkloadPackPackage.GetSourcePackages("", p2)) + // { + // l.Add(x.sourcePackage); + // } + //} + + //int y = packs.Count; + } + + [WindowsOnlyFact] public static void ItCanCreateWorkloads() { 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 5484b5e3298..e2519ef6a35 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 @@ -43,6 +43,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -66,6 +103,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 41d56d7768d..fb897a08809 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -9,9 +9,11 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; using Microsoft.NET.Sdk.WorkloadManifestReader; using WixToolset.Dtf.WindowsInstaller; using Xunit; +using System.Collections.Generic; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -147,7 +149,7 @@ public void ItCanBuildAManifestMsi() Assert.Equal("x64;1033", si.Template); // There should be no version directory present if the old upgrade model is used. - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir", + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir", "because the manifest MSI supports major upgrades"); // Verify that the wixpack archive was created. @@ -188,5 +190,66 @@ public void ItCanBuildATemplatePackMsi() MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("PackageDir", "because it's a template pack"); MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); } + + [WindowsOnlyFact] + public void ItCanBuildAWorkPackGroupMsi() + { + string outputDirectory = GetTestCaseDirectory(); + string packageContentsDirectory = Path.Combine(outputDirectory, "pkg"); + string msiOutputDirectory = Path.Combine(outputDirectory, "msi"); + string packageSource = Path.Combine(TestAssetsPath, "wasm"); + + TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")); + WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, new Version("1.2.3")); + // Parse the manifest to extract information related to workload packs so we can extract a specific pack. + WorkloadManifest manifest = manifestPackage.GetManifest(); + WorkloadId workloadId = new("wasm-tools"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads[workloadId]; + + string packGroupId = null; + WorkloadPackGroupJson packGroupJson = null; + + packGroupId = WorkloadPackGroupPackage.GetPackGroupID(workload.Id); + packGroupJson = new WorkloadPackGroupJson() + { + GroupPackageId = packGroupId, + GroupPackageVersion = manifestPackage.PackageVersion.ToString() + }; + + List workloadPackPackages = []; + + foreach (WorkloadPackId packId in workload.Packs) + { + WorkloadPack pack = manifest.Packs[packId]; + + packGroupJson.Packs.Add(new WorkloadPackJson() + { + PackId = pack.Id, + PackVersion = pack.Version + }); + + string sourcePackage = WorkloadPackPackage.GetSourcePackage(packageSource, pack, "x64"); + + if (!string.IsNullOrWhiteSpace(sourcePackage)) + { + workloadPackPackages.Add(WorkloadPackPackage.Create(pack, sourcePackage, ["x64"], + packageContentsDirectory, null, null)); + } + } + + var groupPackage = new WorkloadPackGroupPackage(workload.Id); + groupPackage.Packs.AddRange(workloadPackPackages); + groupPackage.ManifestsPerPlatform["x64"] = new([manifestPackage]); + + var buildEngine = new MockBuildEngine(); + + foreach (var p in workloadPackPackages) + { + p.Extract(); + } + + WorkloadPackGroupMsi msi = new(groupPackage, "x64", buildEngine, outputDirectory, overridePackageVersions: true); + msi.Build(msiOutputDirectory); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs index bc42b2ce64d..67b927fafb7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -84,6 +84,9 @@ public ITaskItem[] WorkloadManifestPackageFiles set; } + /// + /// Aggregates multiple packs into a single installer. + /// public bool CreateWorkloadPackGroups { get; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index f80c7bbf489..c59dc98eb2c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -73,6 +73,8 @@ static EmbeddedTemplates() { "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" }, { "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" }, { "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" }, + { "PackDirectories.wxs", $"{ns}.MsiTemplate.PackDirectories.wxs" }, + { "DirectoryReference.wxs", $"{ns}.MsiTemplate.DirectoryReference.wxs" }, { "Product.wxs", $"{ns}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{ns}.MsiTemplate.Registry.wxs" }, { "Directory.Build.targets", $"{ns}.MsiTemplate.Directory.Build.targets.pp" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 858ba584322..53104ae1a34 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -21,6 +21,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi /// internal abstract class MsiBase { + /// + /// Used to track the number of directories created. + /// + private int _dirCount = 0; + /// /// The Arcade package that contains the CreateWixBuildWixpack task to support signing. /// @@ -323,17 +328,20 @@ public virtual ITaskItem Build(string outputPath) string wixProjectPath = Path.Combine(WixSourceDirectory, "msi.wixproj"); WixProject wixproj = CreateProject(); wixproj.AddProperty("OutputPath", outputPath); + string directoryBuildTargets = EmbeddedTemplates.Extract("Directory.Build.targets", WixSourceDirectory); if (GenerateWixpack) { // Wixpacks need to capture compile time information from the WiX SDK to rebuild the MSI // after replacing any unsigned content when using Arcade to sign. - Utils.StringReplace(EmbeddedTemplates.Extract("Directory.Build.targets", WixSourceDirectory), + Utils.StringReplace(directoryBuildTargets, Encoding.UTF8, ("__WIXPACK_OUTPUT_DIR__", WixpackOutputDirectory)); // Add a package reference to pull in the CreateWixBuildWixpack task. The version // should automatically default to the "major.minor.path-*", e.g. 10.0.0-* wixproj.AddPackageReference(_MicrosoftDotNetBuildTaskInstallers, ToolsetInfo.ArcadeVersion); + + wixproj.AddProperty(WixProperties.GenerateWixpack, "true"); } if (File.Exists(wixProjectPath)) @@ -393,6 +401,41 @@ protected void AddDefaultPackageFiles(ITaskItem msi) NuGetPackageFiles["LICENSE.TXT"] = @"\"; } + + /// + /// Creates a source file containing a directory fragment. + /// + /// The name of the directory. + /// The ID of the directory. + /// The ID of the directory reference (parent directory). + + protected void AddDirectory(string name, string id, string reference) + { + try + { + AddDirectory(name, id, reference, WixSourceDirectory, $"dir{_dirCount}.wxs"); + } + finally + { + _dirCount++; + } + } + + /// + /// Creates a source file containing a directory fragment. + /// + /// The name of the directory. + /// The ID of the directory. + /// The ID of the directory reference (parent directory). + /// The source directory to use for the generated fragment. + /// The file name of the generated fragment. + internal static void AddDirectory(string name, string id, string reference, string sourceDirectory, string fragmentName) + { + string dirWxs = EmbeddedTemplates.Extract("DirectoryReference.wxs", sourceDirectory, fragmentName); + + Utils.StringReplace(dirWxs, Encoding.UTF8, + (MsiTokens.__DIR_REF_ID__, reference), (MsiTokens.__DIR_ID__, id), (MsiTokens.__DIR_NAME__, name)); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs new file mode 100644 index 00000000000..91002bb059d --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines token names used to create MSIs. + /// + internal class MsiTokens + { + public static readonly string __DIR_REF_ID__ = nameof(__DIR_REF_ID__); + public static readonly string __DIR_ID__ = nameof(__DIR_ID__); + public static readonly string __DIR_NAME__ = nameof(__DIR_NAME__); + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs index b8948f2aff2..34f7d9cab03 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/PayloadPackageTokens.wix.cs @@ -1,10 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Text; - namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi { /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index 1ea448de310..b95f28dcefe 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -10,6 +10,7 @@ using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; + using Microsoft.NET.Sdk.WorkloadManifestReader; namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi @@ -18,21 +19,28 @@ internal class WorkloadPackGroupMsi : MsiBase { WorkloadPackGroupPackage _package; + int _dirIdCount = 1; + /// protected override string BaseOutputName => Metadata.Id; - protected override string ProviderKeyName => + protected override string ProviderKeyName => $"{_package.Id},{Metadata.PackageVersion},{Platform}"; protected override string? InstallationRecordKey => "InstalledPackGroups"; - protected override Guid UpgradeCode => + protected override Guid UpgradeCode => Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}"); + /// + /// Gets a new directory ID. + /// + protected string DirectoryId => $"dir_{_dirIdCount++:0000}"; + public WorkloadPackGroupMsi(WorkloadPackGroupPackage package, string platform, IBuildEngine buildEngine, string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, bool overridePackageVersions = false, bool generateWixPack = false) - : base(package.GetMsiMetadata(), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion, + : base(package.GetMsiMetadata(), buildEngine, platform, baseIntermediatOutputPath, wixToolsetVersion, overridePackageVersions, generateWixPack) { _package = package; @@ -42,130 +50,194 @@ protected override WixProject CreateProject() { WixProject wixproj = base.CreateProject(); + string wixProjectPath = Path.Combine(WixSourceDirectory, "packgroup.wixproj"); + + EmbeddedTemplates.Extract("PackDirectories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); + + // Extract and modify the installation record. For pack groups, we need to add an entry for each + // workload pack installed by the group. + string registryWxsPath = EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + var registryDoc = XDocument.Load(registryWxsPath); + +#nullable disable + if (registryDoc != null) + { + var ns = registryDoc.Root.Name.Namespace; + var registryKeyElement = registryDoc.Root.Descendants(ns + "RegistryKey").Single(); + foreach (var pack in _package.Packs) + { + registryKeyElement.Add(new XElement(ns + "RegistryKey", new XAttribute("Key", pack.Id), + new XElement(ns + "RegistryKey", new XAttribute("Key", pack.PackageVersion), + new XElement(ns + "RegistryValue", new XAttribute("Value", ""), new XAttribute("Type", "string"))))); + } + registryDoc.Save(registryWxsPath); + } +#nullable enable + + int packNumber = 1; + + HashSet directoryReferences = new(); + + foreach (var pack in _package.Packs) + { + // wixproj.AddHarvestDirectory(pack.DestinationDirectory,,, $"CG_PackageContents_{packNumber}"); + + // Calculate the installation directory for the pack and generate a unique reference + string packInstallDir = WorkloadPackMsi.GetInstallDir(pack.Kind); + string packInstallDirReference = WorkloadPackMsi.GetDirectoryReference(pack.Kind); + + if (pack.Kind != WorkloadPackKind.Library && pack.Kind != WorkloadPackKind.Template) + { + // Add directories for the package ID and version under the installation folder. + string dirId = DirectoryId; + AddDirectory(pack.Id, dirId, packInstallDirReference); + packInstallDirReference = DirectoryId; + AddDirectory(pack.PackageVersion.ToString(), packInstallDirReference, dirId); + } + + string sourceDir = $"SourceDir_{packNumber}"; + wixproj.AddHarvestDirectory(pack.DestinationDirectory, packInstallDirReference, + sourceDir, $"CG_PackageContents_{packNumber}"); + wixproj.AddPreprocessorDefinition(sourceDir, pack.DestinationDirectory); + packNumber++; + } +#nullable disable + // Replace single ComponentGroupRef from Product.wxs with a ref for each pack + string productWxsPath = EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory); + var productDoc = XDocument.Load(productWxsPath); + var ns2 = productDoc.Root.Name.Namespace; + var componentGroupRefElement = productDoc.Root.Descendants(ns2 + "ComponentGroupRef").Single(); + componentGroupRefElement.ReplaceWith(Enumerable.Range(1, _package.Packs.Count).Select(n => new XElement(ns2 + "ComponentGroupRef", new XAttribute("Id", "CG_PackageContents_" + n)))); + productDoc.Save(productWxsPath); +#nullable enable + return wixproj; } -// 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. - - -//// 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, OutputName); - -// ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); - -// AddDefaultPackageFiles(msi); - -// return msi; -// } + // 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. + + + //// 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, OutputName); + + // ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); + + // AddDefaultPackageFiles(msi); + + // return msi; + // } class MsiDirectory { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 615f16381af..141aa466654 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -47,22 +47,22 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("WorkloadPackDirectories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); - + string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? "InstallDir" : "VersionDir"; wixproj.AddHarvestDirectory(_package.DestinationDirectory, directoryReference, - PreprocessorDefinitionNames.SourceDir); + PreprocessorDefinitionNames.SourceDir); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); return wixproj; - } + } /// - /// Get the installation directory based on the kind of workload pack. + /// Gets the name of the installation directory based on the kind of workload pack. /// /// The workload pack kind. /// The name of the root installation directory. @@ -75,6 +75,21 @@ internal static string GetInstallDir(WorkloadPackKind kind) => WorkloadPackKind.Tool => "tool-packs", _ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, kind)), }; + + /// + /// Gets the directory reference (ID) associated with the workload pack kind. + /// + /// The workload pack kind. + /// The directory reference (ID) of the installation directory. + internal static string GetDirectoryReference(WorkloadPackKind kind) => + kind switch + { + WorkloadPackKind.Framework or WorkloadPackKind.Sdk => "PacksDir", + WorkloadPackKind.Library => "LibraryPacksDir", + WorkloadPackKind.Template => "TemplatePacksDir", + WorkloadPackKind.Tool => "ToolPacksDir", + _ => throw new ArgumentException(string.Format(Strings.UnknownWorkloadKind, kind)), + }; } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp index 15859dd19b6..a03187e1fe4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directory.Build.targets.pp @@ -1,6 +1,6 @@ - + $(IntermediateOutputPath)wixpack __WIXPACK_OUTPUT_DIR__ @@ -23,4 +23,11 @@ + + + + + $(CompilerAdditionalOptions) -bcgg + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs new file mode 100644 index 00000000000..1e4b8a4c5fa --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/DirectoryReference.wxs @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs new file mode 100644 index 00000000000..9099f628246 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/PackDirectories.wxs @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs index a464d1260bb..9a98ed90409 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProperties.cs @@ -33,5 +33,10 @@ internal class WixProperties /// The debug information to emit. /// public static readonly string DebugType = nameof(DebugType); + + /// + /// Boolean property indicating whether to generate WiX pack used for signing. + /// + public static readonly string GenerateWixpack = nameof(GenerateWixpack); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs index dde510726d3..a07f2f5f436 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadPackPackage.wix.cs @@ -35,7 +35,7 @@ public string[] Platforms get; } - public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platforms, string destinationBaseDirectory, + public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platforms, string destinationBaseDirectory, ITaskItem[]? shortNames = null, TaskLoggingHelper? log = null) : base(packagePath, destinationBaseDirectory, shortNames, log) { _pack = pack; @@ -100,6 +100,46 @@ public WorkloadPackPackage(WorkloadPack pack, string packagePath, string[] platf } } + /// + /// Gets the package associated with a specific workload pack for the specified platform. + /// + /// The path where workload packages reside. + /// + /// + internal static string? GetSourcePackage(string packageSource, WorkloadPack pack, string platform) + { + if (pack.IsAlias && pack.AliasTo != null) + { + foreach (string rid in pack.AliasTo.Keys) + { + string sourcePackage = Path.Combine(packageSource, $"{pack.AliasTo[rid]}.{pack.Version}.nupkg"); + + switch (rid) + { + case "win7": + case "win10": + case "win": + case "any": + return sourcePackage; + default: + if (rid == $"win-{platform}") + { + return sourcePackage; + } + // Unsupported RID. + continue; + } + } + } + else + { + // For non-RID specific packs we'll produce MSIs for each supported platform. + return Path.Combine(packageSource, $"{pack.Id}.{pack.Version}.nupkg"); + } + + return null; + } + /// /// Creates a workload pack package from the provided NuGet package and workload pack. /// From 29b32c6561602f97b3534cd00ae818098fdb8dbd Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Thu, 4 Dec 2025 13:10:34 -0800 Subject: [PATCH 16/26] Add verification for workload pack group test --- .../MsiTests.cs | 39 +++- ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 2 +- .../src/Msi/ComponentRow.wix.cs | 78 ++++++++ .../src/Msi/MsiUtils.wix.cs | 25 +++ .../src/Msi/WorkloadPackGroupMsi.wix.cs | 173 +----------------- 5 files changed, 141 insertions(+), 176 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index fb897a08809..b8223688d2f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using FluentAssertions; @@ -9,11 +10,10 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; -using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; using Microsoft.NET.Sdk.WorkloadManifestReader; using WixToolset.Dtf.WindowsInstaller; using Xunit; -using System.Collections.Generic; +using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -164,7 +164,7 @@ public void ItCanBuildATemplatePackMsi() string pkgDirectory = Path.Combine(outputDirectory, "pkg"); string msiDirectory = Path.Combine(outputDirectory, "msi"); WorkloadPack templatePack = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null); - TemplatePackPackage pkg = new(templatePack, packagePath, new[] { "x64" }, pkgDirectory); + TemplatePackPackage pkg = new(templatePack, packagePath, ["x64"], pkgDirectory); pkg.Extract(); var buildEngine = new MockBuildEngine(); WorkloadPackMsi msi = new(pkg, "x64", buildEngine, WixToolsetPath, outputDirectory, overridePackageVersions: true); @@ -242,14 +242,43 @@ public void ItCanBuildAWorkPackGroupMsi() groupPackage.ManifestsPerPlatform["x64"] = new([manifestPackage]); var buildEngine = new MockBuildEngine(); - + foreach (var p in workloadPackPackages) { p.Extract(); } WorkloadPackGroupMsi msi = new(groupPackage, "x64", buildEngine, outputDirectory, overridePackageVersions: true); - msi.Build(msiOutputDirectory); + ITaskItem msiItem = msi.Build(msiOutputDirectory); + string msiPath = msiItem.GetMetadata(Metadata.FullPath); + + // Build individual pack MSIs to compare against the pack group. + var sdkPackPackage = workloadPackPackages.FirstOrDefault(p => p.Id == "Microsoft.NET.Runtime.WebAssembly.Sdk"); + WorkloadPackMsi sdkPackMsi = new(sdkPackPackage, "x64", buildEngine, WixToolsetPath, outputDirectory, overridePackageVersions: true); + ITaskItem sdkPackMsiItem = sdkPackMsi.Build(msiOutputDirectory); + string sdkPackMsiPath = sdkPackMsiItem.GetMetadata(Metadata.FullPath); + + // Verify workdload record keys for the pack group. + MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Runtime.WebAssembly.Sdk\10.0.0"); + MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NET.Sdk.WebAssembly.Pack\10.0.0"); + MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.Mono.browser-wasm\10.0.0"); + MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => + r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\x64\wasm.tools.WorkloadPacks\10.0.100\Microsoft.NETCore.App.Runtime.AOT.win-x64.Cross.browser-wasm\10.0.0"); + + // Verify pack directories + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("PacksDir", "because the pack group contains SDK packs"); + MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("LibraryPacksDir", "because the pack group contains a library pack"); + + // Individual pack MSIs and pack group should have stable IDs for their components. + // Pick a unique file from the File table, then locate the matching component in the pack + // MSI and verify that the pack group MSI contains a component with the same ID. + FileRow f1 = MsiUtils.GetAllFiles(sdkPackMsiPath).First(f => f.FileName.EndsWith("Sdk.props")); + ComponentRow c1 = MsiUtils.GetAllComponents(sdkPackMsiPath).First(c => c.Component == f1.Component_); + MsiUtils.GetAllComponents(msiPath).Should().Contain(c => c.ComponentId == c1.ComponentId, + "Packs and PackGroups should share components"); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Microsoft.DotNet.Build.Tasks.Workloads.csproj index 0e2a9bc3150..48247b7a5f9 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 @@ -77,7 +77,7 @@ - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs new file mode 100644 index 00000000000..3ca0fd7dc8c --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/ComponentRow.wix.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using WixToolset.Dtf.WindowsInstaller; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi +{ + /// + /// Defines a single row inside the Component table of an MSI. + /// + public class ComponentRow + { + /// + /// Identifies the component record. + /// + public string Component + { + get; + set; + } + + /// + /// A string GUID unique to this component, version, and language. + /// + public Guid ComponentId + { + get; + set; + } + + /// + /// External key of an entry in the Directory table. + /// + public string Directory_ + { + get; + set; + } + + public int Attributes + { + get; + set; + } + + public string Condition + { + get; + set; + } + + /// + /// The key path for the component. + /// + public string KeyPath + { + get; + set; + } + + /// + /// Creates a new instance from the given . + /// + /// The record to use. + /// A new component row. + public static ComponentRow Create(Record componentRecord) => + new ComponentRow + { + Component = componentRecord.GetString("Component"), + ComponentId = Guid.Parse(componentRecord.GetString("ComponentId")), + Directory_ = componentRecord.GetString("Directory_"), + Attributes = componentRecord.GetInteger("Attributes"), + Condition = componentRecord.GetString("Condition"), + KeyPath = componentRecord.GetString("KeyPath"), + }; + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs index a54bf9928b9..f5a8e498c29 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiUtils.wix.cs @@ -14,6 +14,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi /// public class MsiUtils { + /// + /// Query string to retrieve all the rows from the MSI Component table. + /// + private const string _getComponentsQuery = "SELECT `Component`, `ComponentId`, `Directory_`, `Attributes`, `Condition`, `KeyPath` FROM `Component`"; + /// /// Query string to retrieve all the rows from the MSI File table. /// @@ -39,6 +44,26 @@ public class MsiUtils /// private const string _getRegistryQuery = "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`"; + + /// + /// Gets an enumeration of all the components inside an MSI. + /// + /// The path of the MSI package to query. + /// And enumeration of all the components. + public static IEnumerable GetAllComponents(string packagePath) + { + using InstallPackage ip = new(packagePath, DatabaseOpenMode.ReadOnly); + using Database db = new(packagePath, DatabaseOpenMode.ReadOnly); + using View componentView = db.OpenView(_getComponentsQuery); + List components = new(); + componentView.Execute(); + foreach (Record componentRecord in componentView) + { + components.Add(ComponentRow.Create(componentRecord)); + } + return components; + } + /// /// Gets an enumeration of all the files inside an MSI. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index b95f28dcefe..9c5ce3409be 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -69,9 +69,9 @@ protected override WixProject CreateProject() 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"))))); + registryKeyElement.Add(new XElement(ns + "RegistryKey", + new XAttribute("Key", $@"{pack.Id}\{pack.PackageVersion}"), + new XElement(ns + "RegistryValue", new XAttribute("Value", ""), new XAttribute("Type", "string")))); } registryDoc.Save(registryWxsPath); } @@ -116,173 +116,6 @@ protected override WixProject CreateProject() return wixproj; } - - // 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. - - - //// 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, OutputName); - - // ITaskItem msi = Link(candle.OutputPath, msiFileName, iceSuppressions); - - // AddDefaultPackageFiles(msi); - - // return msi; - // } - - class MsiDirectory - { - public string Name { get; } - public string Id { get; } - - public Dictionary Subdirectories { get; } = new(); - - public MsiDirectory(string name, string id) - { - Name = name; - Id = id; - } - - 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; - } - } } } From 41daee03488a9d354784681022572d282fdfccde Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Mon, 8 Dec 2025 15:04:57 -0800 Subject: [PATCH 17/26] Unit tests for pack groups, minor cleanup --- .../CreateVisualStudioWorkloadTests.cs | 66 ++++----- ....DotNet.Build.Tasks.Workloads.Tests.csproj | 7 +- .../MsiTests.cs | 10 +- .../src/CreateVisualStudioWorkload.wix.cs | 128 +++++++++--------- .../src/DefaultValues.cs | 20 +++ .../src/Msi/MsiBase.wix.cs | 9 ++ .../src/Msi/WorkloadManifestMsi.wix.cs | 2 + .../src/Msi/WorkloadPackGroupMsi.wix.cs | 4 +- .../src/Msi/WorkloadPackMsi.wix.cs | 2 + .../src/Msi/WorkloadSetMsi.wix.cs | 2 + 10 files changed, 136 insertions(+), 114 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs index efb2dec4df4..fb9b8c727d5 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -2,18 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using FluentAssertions; using Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using WixToolset.Dtf.WindowsInstaller; -using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using WixToolset.Dtf.WindowsInstaller; using Xunit; -using FluentAssertions.Equivalency; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -24,6 +22,7 @@ public class CreateVisualStudioWorkloadTests : TestBase [WindowsOnlyFact] public static void ItCreatesPackGroups() { + string packageSource = Path.Combine(TestAssetsPath, "wasm"); // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up // conflicting sources from previous runs. string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLPG"); @@ -33,9 +32,9 @@ public static void ItCreatesPackGroups() Directory.Delete(baseIntermediateOutputPath, recursive: true); } - ITaskItem[] manifestsPackages = + ITaskItem[] manifestsPackages = { - new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")) + new TaskItem(Path.Combine(packageSource, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")) .WithMetadata(Metadata.MsiVersion, "10.0.456") }; @@ -46,51 +45,32 @@ public static void ItCreatesPackGroups() BaseOutputPath = TestBase.BaseOutputPath, BaseIntermediateOutputPath = baseIntermediateOutputPath, BuildEngine = buildEngine, - CreateWorkloadPackGroups = true, ComponentResources = Array.Empty(), + CreateWorkloadPackGroups = true, + DisableParallelPackageGroupProcessing = false, + IsOutOfSupportInVisualStudio = false, ManifestMsiVersion = null, - PackageSource = TestBase.TestAssetsPath, + PackageSource = packageSource, ShortNames = Array.Empty(), - WixToolsetPath = TestBase.WixToolsetPath, - WorkloadManifestPackageFiles = manifestsPackages, - IsOutOfSupportInVisualStudio = false + WixToolsetPath = WixToolsetPath, + WorkloadManifestPackageFiles = manifestsPackages }; bool result = createWorkloadTask.Execute(); Assert.True(result); - //WorkloadManifestPackage p = new(manifestsPackages[0], Path.Combine(Path.GetTempPath(), "WLPG"), Version.Parse("1.2.3")); - //WorkloadManifest manifest = p.GetManifest(); - - //List packs = new(); - - //foreach (WorkloadDefinition workload in manifest.Workloads.Values) - //{ - // if ((workload is WorkloadDefinition wd) && (wd.Platforms == null || wd.Platforms.Any(platform => platform.StartsWith("win"))) && (wd.Packs != null)) - // { - // foreach (WorkloadPackId packId in wd.Packs) - // { - // packs.Add(manifest.Packs[packId]); - // } - // } - //} - - - - //List l = new List(); - - //foreach (var p2 in packs) - //{ - // foreach (var x in WorkloadPackPackage.GetSourcePackages("", p2)) - // { - // l.Add(x.sourcePackage); - // } - //} + // Verify that the Visual Studio workload components reference workload pack groups. + string componentSwr = File.ReadAllText( + Path.Combine(Path.GetDirectoryName( + createWorkloadTask.SwixProjects.FirstOrDefault( + i => i.ItemSpec.Contains("wasm.tools.10.0.swixproj")).ItemSpec), "component.swr")); + Assert.Contains("vs.dependency id=wasm.tools.WorkloadPacks", componentSwr); - //int y = packs.Count; + // Manifest installers should contain additional JSON files describing pack groups. + ITaskItem manifestMsi = createWorkloadTask.Msis.First(m => m.GetMetadata(Metadata.PackageType) == DefaultValues.ManifestMsi); + MsiUtils.GetAllFiles(manifestMsi.ItemSpec).Should().Contain(f => f.FileName.EndsWith("WorkloadPackGroups.json")); } - [WindowsOnlyFact] public static void ItCanCreateWorkloads() { @@ -131,14 +111,14 @@ public static void ItCanCreateWorkloads() CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() { AllowMissingPacks = true, - BaseOutputPath = TestBase.BaseOutputPath, + BaseOutputPath = BaseOutputPath, BaseIntermediateOutputPath = baseIntermediateOutputPath, BuildEngine = buildEngine, ComponentResources = componentResources, ManifestMsiVersion = null, - PackageSource = TestBase.TestAssetsPath, + PackageSource = TestAssetsPath, ShortNames = shortNames, - WixToolsetPath = TestBase.WixToolsetPath, + WixToolsetPath = WixToolsetPath, WorkloadManifestPackageFiles = manifestsPackages, IsOutOfSupportInVisualStudio = true }; 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 e2519ef6a35..aa37d0ceb12 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 @@ -43,8 +43,8 @@ + - @@ -103,9 +103,8 @@ - - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index b8223688d2f..e25aa580c16 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -197,9 +197,10 @@ public void ItCanBuildAWorkPackGroupMsi() string outputDirectory = GetTestCaseDirectory(); string packageContentsDirectory = Path.Combine(outputDirectory, "pkg"); string msiOutputDirectory = Path.Combine(outputDirectory, "msi"); + string pkgOutputDirectory = Path.Combine(outputDirectory, "nuget"); string packageSource = Path.Combine(TestAssetsPath, "wasm"); - TaskItem packageItem = new(Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")); + TaskItem packageItem = new(Path.Combine(packageSource, "microsoft.net.workload.mono.toolchain.current.manifest-10.0.100.10.0.100.nupkg")); WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, new Version("1.2.3")); // Parse the manifest to extract information related to workload packs so we can extract a specific pack. WorkloadManifest manifest = manifestPackage.GetManifest(); @@ -249,8 +250,11 @@ public void ItCanBuildAWorkPackGroupMsi() } WorkloadPackGroupMsi msi = new(groupPackage, "x64", buildEngine, outputDirectory, overridePackageVersions: true); - ITaskItem msiItem = msi.Build(msiOutputDirectory); - string msiPath = msiItem.GetMetadata(Metadata.FullPath); + ITaskItem msiWorkloadPackGroupOutputItem = msi.Build(msiOutputDirectory); + string msiPath = msiWorkloadPackGroupOutputItem.GetMetadata(Metadata.FullPath); + + MsiPayloadPackageProject csproj = new(msi.Metadata, msiWorkloadPackGroupOutputItem, outputDirectory, pkgOutputDirectory, msi.NuGetPackageFiles); + msiWorkloadPackGroupOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); // Build individual pack MSIs to compare against the pack group. var sdkPackPackage = workloadPackPackages.FirstOrDefault(p => p.Id == "Microsoft.NET.Runtime.WebAssembly.Sdk"); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs index 67b927fafb7..28498d64c10 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -103,12 +103,6 @@ public string PackageSource set; } - public bool UseWorkloadPackGroupsForVS - { - get; - set; - } - /// /// If true, will skip creating MSIs for workload packs if they are part of a pack group /// @@ -118,6 +112,9 @@ public bool SkipRedundantMsiCreation set; } + /// + /// If true, workload pack groups are built sequentially. + /// public bool DisableParallelPackageGroupProcessing { get; @@ -288,7 +285,7 @@ protected override bool ExecuteCore() { string platform = kvp.Key; - // The key is the paths to the packages included in the pack group, sorted in alphabetical order + // The key is the path to the packages included in the pack group, sorted in alphabetical order string uniquePackGroupKey = string.Join("\r\n", kvp.Value.Select(p => p.PackagePath).OrderBy(p => p)); if (!packGroupPackages.TryGetValue(uniquePackGroupKey, out var groupPackage)) { @@ -332,79 +329,86 @@ protected override bool ExecuteCore() } } + // Depulicate packages and extract them. Building and extrating in parallel can cause issues as the same + // source package can be shared across multiple workloads and platforms. + Parallel.ForEach(buildData.Values.Select(d => d.Package).Distinct(), package => + { + Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, package.PackagePath)); + package.Extract(); + }); + List msiItems = new(); List swixProjectItems = new(); - _ = Parallel.ForEach(buildData.Values, data => + if (!CreateWorkloadPackGroups) { - // Extract the contents of the workload pack package. - Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath)); - data.Package.Extract(); - - // Enumerate over the platforms and build each MSI once. - _ = Parallel.ForEach(data.FeatureBands.Keys, platform => + _ = Parallel.ForEach(buildData.Values, data => { - WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem msiOutputItem = msi.Build(MsiOutputPath); - - // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); - msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); + // Extract the contents of the workload pack package. + Log.LogMessage(MessageImportance.Low, string.Format(Strings.BuildExtractingPackage, data.Package.PackagePath)); + data.Package.Extract(); - lock (msiItems) + // Enumerate over the platforms and build each MSI once. + _ = Parallel.ForEach(data.FeatureBands.Keys, platform => { - msiItems.Add(msiOutputItem); - } + WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); - foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) - { - // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch - if (_supportsMachineArch[sdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) - { - MsiSwixProject swixProject = _supportsMachineArch[sdkFeatureBand] ? - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) : - new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio); - string swixProj = swixProject.Create(); + // Generate a .csproj to package the MSI and its manifest for CLI installs. + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); + msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); - ITaskItem swixProjectItem = new TaskItem(swixProj); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); - swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + lock (msiItems) + { + msiItems.Add(msiOutputItem); + } - lock (swixProjectItems) + foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) + { + // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch + if (_supportsMachineArch[sdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) { - swixProjectItems.Add(swixProjectItem); + MsiSwixProject swixProject = _supportsMachineArch[sdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform), outOfSupport: IsOutOfSupportInVisualStudio); + string swixProj = swixProject.Create(); + + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); + swixProjectItem.SetMetadata(Metadata.IsPreview, "false"); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } } } - } + }); }); - }); - - // Parallel processing of pack groups was causing file access errors for heat in an earlier version of this code - // So we support a flag to disable the parallelization if that starts happening again - PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroupPackages.Values, packGroup => + } + else { - foreach (var pack in packGroup.Packs) - { - pack.Extract(); - } - foreach (var platform in packGroup.ManifestsPerPlatform.Keys) + // Parallel processing of pack groups was causing file access errors for heat in an earlier version of this code + // So we support a flag to disable the parallelization if that starts happening again + PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroupPackages.Values, packGroup => { - WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); - ITaskItem msiOutputItem = msi.Build(MsiOutputPath); + foreach (var platform in packGroup.ManifestsPerPlatform.Keys) + { + WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, BaseIntermediateOutputPath); + ITaskItem msiOutputItem = msi.Build(MsiOutputPath); - // Generate a .csproj to package the MSI and its manifest for CLI installs. - MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); - msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); + // Generate a .csproj to package the MSI and its manifest for CLI installs. + MsiPayloadPackageProject csproj = new(msi.Metadata, msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.NuGetPackageFiles); + msiOutputItem.SetMetadata(Metadata.PackageProject, csproj.Create()); - lock (msiItems) - { - msiItems.Add(msiOutputItem); - } + lock (msiItems) + { + msiItems.Add(msiOutputItem); + } - if (UseWorkloadPackGroupsForVS) - { + // Always generate pack groups for VS. PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage => { // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch @@ -427,8 +431,8 @@ protected override bool ExecuteCore() } }); } - } - }); + }); + } // Generate MSIs for the workload manifests along with a .csproj to package the MSI and a SWIX project for // Visual Studio. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index 99522a1302c..b8e3c1dfbf5 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -63,5 +63,25 @@ internal static class DefaultValues /// A value indicating that the SWIX project creates an MSI package for a workload set. /// public static readonly string PackageTypeMsiWorkloadSet = "msi-workload-set"; + + /// + /// A value indicating the MSI represents a workload manifest. + /// + public static readonly string ManifestMsi = "manifest"; + + /// + /// A value indicating the MSI represents a workload pack. + /// + public static readonly string WorkloadPackMsi = "pack"; + + /// + /// A value indicating the MSI represents a workload set. + /// + public static readonly string WorkloadSetMsi = "workload-set"; + + /// + /// A value indicating the MSI represents a workload pack group. + /// + public static readonly string WorkloadPackGroupMsi = "pack-group"; } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 53104ae1a34..999d3edd0b5 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -197,6 +197,14 @@ protected abstract string? InstallationRecordKey get; } + /// + /// The package type represented by the MSI. + /// + protected abstract string? MsiPackageType + { + get; + } + /// /// Creates a new instance of the class. /// @@ -378,6 +386,7 @@ public virtual ITaskItem Build(string outputPath) msiItem.SetMetadata(Workloads.Metadata.Platform, Platform); msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}"); msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId); + msiItem.SetMetadata(Workloads.Metadata.PackageType, MsiPackageType); if (GenerateWixpack && !string.IsNullOrEmpty(WixpackOutputDirectory)) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 37289f5bb0e..89a3b3f83c6 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -26,6 +26,8 @@ internal class WorkloadManifestMsi : MsiBase /// protected override string BaseOutputName => Path.GetFileNameWithoutExtension(Package.PackagePath); + protected override string? MsiPackageType => DefaultValues.ManifestMsi; + /// /// True if the manifest installer supports side-by-side installs, otherwise it's /// assumed the installer supports major upgrades. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index 9c5ce3409be..9573de7d24c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -32,6 +32,8 @@ internal class WorkloadPackGroupMsi : MsiBase protected override Guid UpgradeCode => Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Metadata.Id};{Platform}"); + protected override string? MsiPackageType => DefaultValues.WorkloadPackGroupMsi; + /// /// Gets a new directory ID. /// @@ -83,8 +85,6 @@ protected override WixProject CreateProject() foreach (var pack in _package.Packs) { - // wixproj.AddHarvestDirectory(pack.DestinationDirectory,,, $"CG_PackageContents_{packNumber}"); - // Calculate the installation directory for the pack and generate a unique reference string packInstallDir = WorkloadPackMsi.GetInstallDir(pack.Kind); string packInstallDirReference = WorkloadPackMsi.GetDirectoryReference(pack.Kind); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 141aa466654..23e6aeda3ba 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -26,6 +26,8 @@ internal class WorkloadPackMsi : MsiBase protected override string? InstallationRecordKey => "InstalledPacks"; + protected override string? MsiPackageType => DefaultValues.WorkloadPackMsi; + public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngine buildEngine, string wixToolsetPath, string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, bool overridePackageVersions = false, bool generateWixPack = false, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs index 795a194e95d..716689be2f9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -27,6 +27,8 @@ internal class WorkloadSetMsi : MsiBase protected override Guid UpgradeCode => Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); + protected override string? MsiPackageType => DefaultValues.WorkloadSetMsi; + public WorkloadSetMsi(WorkloadSetPackage package, string platform, IBuildEngine buildEngine, string baseIntermediatOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, bool overridePackageVersions = false, bool generateWixPack = false) : From a92c59b0801eb97e6f588abe4045694203dca989 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Mon, 8 Dec 2025 16:21:03 -0800 Subject: [PATCH 18/26] Add size check for MSI --- .../src/DefaultValues.cs | 9 +++++++++ .../src/Msi/MsiBase.wix.cs | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index b8e3c1dfbf5..7467f6081e7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -8,6 +8,15 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads /// internal static class DefaultValues { + /// + /// Maximum size of an MSI in bytes. + /// + /// + /// Workload MSIs are distributed in NuGet packages and cannot exceed the maximum size of a NuGet package (250 MB). The limit + /// is set to 245 MB to account for package metadata, signatures, etc. + /// + public const int MaxMsiSize = 256901120; + /// /// Default component group identifier used when harvesting a directory. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 999d3edd0b5..1c3eec2f639 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -388,6 +388,12 @@ public virtual ITaskItem Build(string outputPath) msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId); msiItem.SetMetadata(Workloads.Metadata.PackageType, MsiPackageType); + var fi = new FileInfo(msiItem.ItemSpec); + if (fi.Length > DefaultValues.MaxMsiSize) + { + throw new IOException($"The generated MSI, {msiItem.ItemSpec}, exceeded the maximum size ({DefaultValues.MaxMsiSize} bytes allowed for workloads.)"); + } + if (GenerateWixpack && !string.IsNullOrEmpty(WixpackOutputDirectory)) { msiItem.SetMetadata(Workloads.Metadata.Wixpack, Path.Combine( From 0994daa3c2a0f4b191dcc0815ebe07afbd8601b3 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 9 Dec 2025 09:09:14 -0800 Subject: [PATCH 19/26] Fix toolset info generation --- ...rosoft.DotNet.Build.Tasks.Workloads.csproj | 19 ++++++++----------- .../src/ToolsetInfo.cs.pp | 8 ++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp 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 48247b7a5f9..5bde45082f8 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 @@ -67,21 +67,18 @@ + - - - - - - - - - - - + + $([System.IO.File]::ReadAllText(ToolsetInfo.cs.pp)) + $(ToolsetInfoText.Replace('{MicrosoftWixToolsetSdkVersion}', $(MicrosoftWixToolsetSdkVersion))) + $(ToolsetInfoText.Replace('{ArcadeVersion}', $(VersionPrefix)-*)) + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp new file mode 100644 index 00000000000..a33a6edd2af --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/ToolsetInfo.cs.pp @@ -0,0 +1,8 @@ +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + public class ToolsetInfo + { + public const string MicrosoftWixToolsetVersion = "{MicrosoftWixToolsetSdkVersion}"; + public const string ArcadeVersion = "{ArcadeVersion}"; + } +} From 1d993521046c3878278f4c4f551bd1e5030ef4fb Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 9 Dec 2025 14:05:42 -0800 Subject: [PATCH 20/26] Test clean up, address helix failures --- .../CreateVisualStudioWorkloadSetTests.cs | 10 +++--- .../MsiTests.cs | 2 ++ .../testassets/NuGet.config | 35 +++++++++++++++++++ .../src/CreateVisualStudioWorkload.wix.cs | 11 +++--- .../src/CreateVisualStudioWorkloadSet.wix.cs | 6 ++-- .../src/Msi/MsiBase.wix.cs | 14 +++++--- .../src/VisualStudioWorkloadTaskBase.wix.cs | 11 +++++- .../src/Wix/WixProject.cs | 10 ++---- 8 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs index 5e0cad83022..35bb09fc533 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -40,8 +40,7 @@ public void ItCanCreateWorkloadSets() BaseOutputPath = Path.Combine(testCaseDirectory, "msi"), BaseIntermediateOutputPath = baseIntermediateOutputPath, BuildEngine = buildEngine, - OverridePackageVersions = true, - + OverridePackageVersions = true, WixToolsetPath = WixToolsetPath, WorkloadSetPackageFiles = workloadSetPackages }; @@ -50,7 +49,7 @@ public void ItCanCreateWorkloadSets() buildEngine.BuildErrorEvents[0].Message : "Task failed. No error events"); // Spot check the x64 generated MSI. - ITaskItem msi = createWorkloadSetTask.Msis.Where(i => i.GetMetadata(Metadata.Platform) == "x64").FirstOrDefault(); + ITaskItem msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "x64"); Assert.NotNull(msi); // Verify the workload set records the CLI will use. @@ -81,9 +80,8 @@ public void ItCanCreateWorkloadSets() Assert.Contains("vs.package.type=msi", msiSwr); // Verify package group SWIX project - ITaskItem workloadSetPackageGroupSwixItem = createWorkloadSetTask.SwixProjects.Where( - s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeWorkloadSetPackageGroup)). - FirstOrDefault(); + ITaskItem workloadSetPackageGroupSwixItem = createWorkloadSetTask.SwixProjects.FirstOrDefault( + s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeWorkloadSetPackageGroup)); string packageGroupSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(workloadSetPackageGroupSwixItem.ItemSpec), "packageGroup.swr")); Assert.Contains("package name=PackageGroup.NET.Workloads-9.0.100", packageGroupSwr); Assert.Contains("vs.dependency id=Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1", packageGroupSwr); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index e25aa580c16..2720f281462 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -32,6 +32,8 @@ public class MsiTests : TestBase private static ITaskItem BuildManifestMsi(string outputDirectory, string packagePath, string msiVersion = "1.2.3", string platform = "x64", bool allowSideBySideInstalls = true, bool generateWixpack = false, string wixpackOutputDirectory = null) { + Directory.CreateDirectory(outputDirectory); + File.Copy(Path.Combine(TestAssetsPath, "NuGet.config"), Path.Combine(outputDirectory, "NuGet.config"), overwrite: true); TaskItem packageItem = new(packagePath); WorkloadManifestPackage pkg = new(packageItem, Path.Combine(outputDirectory, "pkg"), new Version(msiVersion)); pkg.Extract(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config new file mode 100644 index 00000000000..a20efd04673 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/testassets/NuGet.config @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs index 28498d64c10..cf4a6554f31 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -166,12 +166,13 @@ protected override bool ExecuteCore() Dictionary manifestMsisByPlatform = new(); foreach (string platform in SupportedPlatforms) { - var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, BaseIntermediateOutputPath, EnableSideBySideManifests); + var manifestMsi = new WorkloadManifestMsi(manifestPackage, platform, BuildEngine, BaseIntermediateOutputPath, + EnableSideBySideManifests, generateWixpack: GenerateWixPack); manifestMsisToBuild.Add(manifestMsi); manifestMsisByPlatform[platform] = manifestMsi; } - // If we're supporting SxS manifests, generate a package group to wrap the manifest VS packages + // If we're supporting SxS manifests, a package group to wrap the manifest VS packages // so we don't deal with unstable package IDs during VS insertions. if (EnableSideBySideManifests) { @@ -351,7 +352,8 @@ protected override bool ExecuteCore() // Enumerate over the platforms and build each MSI once. _ = Parallel.ForEach(data.FeatureBands.Keys, platform => { - WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath); + WorkloadPackMsi msi = new(data.Package, platform, BuildEngine, WixToolsetPath, BaseIntermediateOutputPath, + generateWixPack: GenerateWixPack); ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Generate a .csproj to package the MSI and its manifest for CLI installs. @@ -396,7 +398,8 @@ protected override bool ExecuteCore() { foreach (var platform in packGroup.ManifestsPerPlatform.Keys) { - WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, BaseIntermediateOutputPath); + WorkloadPackGroupMsi msi = new(packGroup, platform, BuildEngine, BaseIntermediateOutputPath, + generateWixPack: GenerateWixPack); ITaskItem msiOutputItem = msi.Build(MsiOutputPath); // Generate a .csproj to package the MSI and its manifest for CLI installs. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs index d05afbf12c7..20430ef46fe 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkloadSet.wix.cs @@ -51,7 +51,8 @@ protected override bool ExecuteCore() foreach (string platform in SupportedPlatforms) { var workloadSetMsi = new WorkloadSetMsi(workloadSetPackage, platform, BuildEngine, - BaseIntermediateOutputPath, overridePackageVersions: OverridePackageVersions); + BaseIntermediateOutputPath, overridePackageVersions: OverridePackageVersions, + generateWixPack: GenerateWixPack); workloadSetMsisToBuild.Add(workloadSetMsi); } @@ -80,7 +81,8 @@ protected override bool ExecuteCore() // Generate a .swixproj for packaging the MSI in Visual Studio. We'll default to using machineArch always. Workload sets // are being introduced in .NET 8 and the corresponding versions of VS all support the machineArch property. - MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, workloadSetPackage.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)); + MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, workloadSetPackage.SdkFeatureBand, + chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)); string swixProj = swixProject.Create(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 1c3eec2f639..490f52cc8e1 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -150,7 +150,7 @@ protected bool GenerateWixpack } /// - /// The package version to use when adding package references to the .wixproj. Returns + /// The package version to use when adding package references to the generated .wixproj. Returns /// if is . /// protected string? WixToolsetPackageVersion => @@ -210,14 +210,17 @@ protected abstract string? MsiPackageType /// /// Metadata passed to the task that are used to build the MSI. /// - /// - /// + /// The target platform of the MSI. + /// The base directory to use when generating the wix project source files. /// The version of the WiX toolset to use for building the installer. - /// + /// Determines whether PackageOverride attributes should be generated + /// when adding package references to avoid CPM conflicts. + /// When set to , package references won't include + /// package version information, unless version overrides are enabled. public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string platform, string baseIntermediateOutputPath, string wixToolsetVersion = ToolsetInfo.MicrosoftWixToolsetVersion, bool overridePackageVersions = false, bool generateWixpack = false, - string? wixpackOutputDirectory = null) + string? wixpackOutputDirectory = null, bool managePackageVersionsCentrally = false) { BuildEngine = buildEngine; Platform = platform; @@ -228,6 +231,7 @@ public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, OverridePackageVersions = overridePackageVersions; GenerateWixpack = generateWixpack; WixpackOutputDirectory = wixpackOutputDirectory; + ManagePackageVersionsCentrally = managePackageVersionsCentrally; } /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs index 49df0e34c44..4314896a6be 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/VisualStudioWorkloadTaskBase.wix.cs @@ -20,7 +20,7 @@ public abstract class VisualStudioWorkloadTaskBase : Task public static readonly string[] SupportedPlatforms = { "x86", "x64", "arm64" }; /// - /// The root intermediate output directory. This directory serves as a the base for generating + /// The root intermediate output directory. This directory serves as the base for generating /// installer sources and other projects used to create workload artifacts for Visual Studio. /// [Required] @@ -40,6 +40,15 @@ public string BaseOutputPath set; } + /// + /// Determines whether a wix pack archive should be generated to sign the MSI using Arcade. + /// + public bool GenerateWixPack + { + get; + set; + } = false; + /// /// 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 diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs index 10318366678..fc8690671ea 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixProject.cs @@ -114,14 +114,8 @@ public void Save(string path) // Allow null/empty versions in case CPM already defined the packages. if (!string.IsNullOrEmpty(_packageReferences[packageId])) { - if (OverridePackageVersions) - { - item.SetAttribute(_attributeVersionOverride, _packageReferences[packageId]); - } - else - { - item.SetAttribute(_attributeVersion, _packageReferences[packageId]); - } + item.SetAttribute(OverridePackageVersions ? _attributeVersionOverride : _attributeVersion, + _packageReferences[packageId]); } packageReferencesItemGroup.AppendChild(item); From 4300620659dd0df590092fde800b747ffac59100 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 9 Dec 2025 20:26:46 -0800 Subject: [PATCH 21/26] Separate tests to avoid shared access --- .../CreateVisualStudioWorkloadTests.cs | 240 +---------------- .../EmscriptenTests.cs | 255 ++++++++++++++++++ 2 files changed, 256 insertions(+), 239 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs index fb9b8c727d5..635ce486ef7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using FluentAssertions; @@ -10,7 +9,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; -using WixToolset.Dtf.WindowsInstaller; using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests @@ -18,7 +16,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests [Collection("Workload Creation")] public class CreateVisualStudioWorkloadTests : TestBase { - [SkipOnCI(reason: "This test creates workload pack groups for WASM. The test requires almost 1GB of packages and takes a few minutes to run.")] + [SkipOnCI(reason: "This test builds the full WASM workload.")] [WindowsOnlyFact] public static void ItCreatesPackGroups() { @@ -70,241 +68,5 @@ public static void ItCreatesPackGroups() ITaskItem manifestMsi = createWorkloadTask.Msis.First(m => m.GetMetadata(Metadata.PackageType) == DefaultValues.ManifestMsi); MsiUtils.GetAllFiles(manifestMsi.ItemSpec).Should().Contain(f => f.FileName.EndsWith("WorkloadPackGroups.json")); } - - [WindowsOnlyFact] - public static void ItCanCreateWorkloads() - { - // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up - // conflicting sources from previous runs. - string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WL"); - - if (Directory.Exists(baseIntermediateOutputPath)) - { - Directory.Delete(baseIntermediateOutputPath, recursive: true); - } - - ITaskItem[] manifestsPackages = new[] - { - new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) - .WithMetadata(Metadata.MsiVersion, "6.33.28") - }; - - ITaskItem[] componentResources = new[] - { - new TaskItem("microsoft-net-sdk-emscripten") - .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") - .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") - .WithMetadata(Metadata.Version, "5.6.7.8") - }; - - ITaskItem[] shortNames = new[] - { - new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), - new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") - }; - - IBuildEngine buildEngine = new MockBuildEngine(); - - CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() - { - AllowMissingPacks = true, - BaseOutputPath = BaseOutputPath, - BaseIntermediateOutputPath = baseIntermediateOutputPath, - BuildEngine = buildEngine, - ComponentResources = componentResources, - ManifestMsiVersion = null, - PackageSource = TestAssetsPath, - ShortNames = shortNames, - WixToolsetPath = WixToolsetPath, - WorkloadManifestPackageFiles = manifestsPackages, - IsOutOfSupportInVisualStudio = true - }; - - bool result = createWorkloadTask.Execute(); - - Assert.True(result); - ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-x64.msi")).FirstOrDefault(); - Assert.NotNull(manifestMsiItem); - - // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation. - // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band, - Assert.Equal("{C4F269D9-6B65-36C5-9556-75B78EFE9EDA}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode)); - // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would - // be generated. - Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion)); - Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,x64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec)); - - // Process the template in the summary information stream. This is the only way to verify the intended platform - // of the MSI itself. - using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false); - Assert.Equal("x64;1033", si.Template); - - // Verify the SWIX authoring for the component representing the workload in VS. The first should be a standard - // component. There should also be a second preview component. - string componentSwr = File.ReadAllText( - Path.Combine(Path.GetDirectoryName( - createWorkloadTask.SwixProjects.FirstOrDefault( - i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr")); - Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); - string previewComponentSwr = File.ReadAllText( - Path.Combine(Path.GetDirectoryName( - createWorkloadTask.SwixProjects.FirstOrDefault( - i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr")); - Assert.Contains("package name=microsoft.net.sdk.emscripten.pre", previewComponentSwr); - - // Emscripten is an abstract workload so it should be a component group. - Assert.Contains("vs.package.type=component", componentSwr); - Assert.Contains("vs.package.outOfSupport=yes", componentSwr); - Assert.Contains("isUiGroup=yes", componentSwr); - Assert.Contains("version=5.6.7.8", componentSwr); - - Assert.Contains("vs.package.type=component", previewComponentSwr); - Assert.Contains("isUiGroup=yes", previewComponentSwr); - Assert.Contains("version=5.6.7.8", previewComponentSwr); - - // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased - // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the - // aliased workload pack packages. - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); - - // Pack dependencies for preview components should be identical to the non-preview component. - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", previewComponentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", previewComponentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", previewComponentSwr); - - // Verify the SWIX authoring for the VS package wrapping the manifest MSI - string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "x64", "msi.swr")); - Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); - Assert.Contains("vs.package.type=msi", manifestMsiSwr); - Assert.Contains("vs.package.chip=x64", manifestMsiSwr); - Assert.DoesNotContain("vs.package.machineArch", manifestMsiSwr); - Assert.DoesNotContain("vs.package.outOfSupport", manifestMsiSwr); - - // Verify that no arm64 MSI authoring for VS. EMSDK doesn't define RIDs for arm64, but manifests always generate - // arm64 MSIs for the CLI based installs so we should not see that. - string swixRootDirectory = Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200"); - IEnumerable arm64Directories = Directory.EnumerateDirectories(swixRootDirectory, "arm64", SearchOption.AllDirectories); - Assert.DoesNotContain(arm64Directories, s => s.Contains("arm64")); - - // Verify the SWIX authoring for one of the workload pack MSIs. Packs get assigned random sub-folders so we - // need to filter out the SWIX project output items the task produced. - ITaskItem pythonPackSwixItem = createWorkloadTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.Emscripten.Python.6.0.4\x64")).FirstOrDefault(); - string packMsiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(pythonPackSwixItem.ItemSpec), "msi.swr")); - Assert.Contains("package name=Microsoft.Emscripten.Python.6.0.4", packMsiSwr); - Assert.Contains("vs.package.chip=x64", packMsiSwr); - Assert.Contains("vs.package.outOfSupport=yes", packMsiSwr); - Assert.DoesNotContain("vs.package.machineArch", packMsiSwr); - - // Verify the swix project items for components. The project files names always contain the major.minor suffix, so we'll end up - // with microsoft.net.sdk.emscripten.5.6.swixproj and microsoft.net.sdk.emscripten.pre.5.6.swixproj - IEnumerable swixComponentProjects = createWorkloadTask.SwixProjects.Where(s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeComponent)); - Assert.All(swixComponentProjects, c => Assert.True(c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "true" || - !c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "false")); - } - - [WindowsOnlyFact] - public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio() - { - // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up - // conflicting sources from previous runs. - string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLa64"); - - if (Directory.Exists(baseIntermediateOutputPath)) - { - Directory.Delete(baseIntermediateOutputPath, recursive: true); - } - - ITaskItem[] manifestsPackages = new[] - { - new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) - .WithMetadata(Metadata.MsiVersion, "6.33.28") - .WithMetadata(Metadata.SupportsMachineArch, "true") - }; - - ITaskItem[] componentResources = new[] - { - new TaskItem("microsoft-net-sdk-emscripten") - .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") - .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") - .WithMetadata(Metadata.Version, "5.6.7.8") - }; - - ITaskItem[] shortNames = new[] - { - new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), - new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), - new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") - }; - - IBuildEngine buildEngine = new MockBuildEngine(); - - CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() - { - AllowMissingPacks = true, - BaseOutputPath = TestBase.BaseOutputPath, - BaseIntermediateOutputPath = baseIntermediateOutputPath, - BuildEngine = buildEngine, - ComponentResources = componentResources, - ManifestMsiVersion = null, - PackageSource = TestBase.TestAssetsPath, - ShortNames = shortNames, - WixToolsetPath = TestBase.WixToolsetPath, - WorkloadManifestPackageFiles = manifestsPackages, - }; - - bool result = createWorkloadTask.Execute(); - - Assert.True(result); - ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-arm64.msi")).FirstOrDefault(); - Assert.NotNull(manifestMsiItem); - - // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation. - // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band, - Assert.Equal("{CBA7CF4A-F3C9-3B75-8F1F-0D08AF7CD7BE}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode)); - // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would - // be generated. - Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion)); - Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,arm64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec)); - - // Process the template in the summary information stream. This is the only way to verify the intended platform - // of the MSI itself. - using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false); - Assert.Equal("Arm64;1033", si.Template); - - // Verify the SWIX authoring for the component representing the workload in VS. - string componentSwr = File.ReadAllText( - Path.Combine(Path.GetDirectoryName( - createWorkloadTask.SwixProjects.FirstOrDefault( - i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr")); - Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); - - // Emscripten is an abstract workload so it should be a component group. - Assert.Contains("vs.package.type=component", componentSwr); - Assert.Contains("isUiGroup=yes", componentSwr); - Assert.Contains("version=5.6.7.8", componentSwr); - // Default setting should be off - Assert.Contains("vs.package.outOfSupport=no", componentSwr); - - // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased - // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the - // aliased workload pack packages. - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr); - Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); - - // Verify the SWIX authoring for the VS package wrapping the manifest MSI - string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "arm64", "msi.swr")); - Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); - Assert.Contains("vs.package.type=msi", manifestMsiSwr); - Assert.DoesNotContain("vs.package.chip", manifestMsiSwr); - Assert.Contains("vs.package.machineArch=arm64", manifestMsiSwr); - } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs new file mode 100644 index 00000000000..25accdf44e8 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Arcade.Test.Common; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using WixToolset.Dtf.WindowsInstaller; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests +{ + [Collection("Emscripten")] + public class EmscriptenTests : TestBase + { + [WindowsOnlyFact] + public static void ItCanCreateWorkloads() + { + // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up + // conflicting sources from previous runs. + string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WL"); + + if (Directory.Exists(baseIntermediateOutputPath)) + { + Directory.Delete(baseIntermediateOutputPath, recursive: true); + } + + ITaskItem[] manifestsPackages = new[] + { + new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) + .WithMetadata(Metadata.MsiVersion, "6.33.28") + }; + + ITaskItem[] componentResources = new[] + { + new TaskItem("microsoft-net-sdk-emscripten") + .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") + .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") + .WithMetadata(Metadata.Version, "5.6.7.8") + }; + + ITaskItem[] shortNames = new[] + { + new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), + new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") + }; + + IBuildEngine buildEngine = new MockBuildEngine(); + + CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() + { + AllowMissingPacks = true, + BaseOutputPath = BaseOutputPath, + BaseIntermediateOutputPath = baseIntermediateOutputPath, + BuildEngine = buildEngine, + ComponentResources = componentResources, + ManifestMsiVersion = null, + PackageSource = TestAssetsPath, + ShortNames = shortNames, + WixToolsetPath = WixToolsetPath, + WorkloadManifestPackageFiles = manifestsPackages, + IsOutOfSupportInVisualStudio = true + }; + + bool result = createWorkloadTask.Execute(); + + Assert.True(result); + ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-x64.msi")).FirstOrDefault(); + Assert.NotNull(manifestMsiItem); + + // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation. + // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band, + Assert.Equal("{C4F269D9-6B65-36C5-9556-75B78EFE9EDA}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode)); + // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would + // be generated. + Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion)); + Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,x64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec)); + + // Process the template in the summary information stream. This is the only way to verify the intended platform + // of the MSI itself. + using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false); + Assert.Equal("x64;1033", si.Template); + + // Verify the SWIX authoring for the component representing the workload in VS. The first should be a standard + // component. There should also be a second preview component. + string componentSwr = File.ReadAllText( + Path.Combine(Path.GetDirectoryName( + createWorkloadTask.SwixProjects.FirstOrDefault( + i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); + string previewComponentSwr = File.ReadAllText( + Path.Combine(Path.GetDirectoryName( + createWorkloadTask.SwixProjects.FirstOrDefault( + i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.emscripten.pre", previewComponentSwr); + + // Emscripten is an abstract workload so it should be a component group. + Assert.Contains("vs.package.type=component", componentSwr); + Assert.Contains("vs.package.outOfSupport=yes", componentSwr); + Assert.Contains("isUiGroup=yes", componentSwr); + Assert.Contains("version=5.6.7.8", componentSwr); + + Assert.Contains("vs.package.type=component", previewComponentSwr); + Assert.Contains("isUiGroup=yes", previewComponentSwr); + Assert.Contains("version=5.6.7.8", previewComponentSwr); + + // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased + // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the + // aliased workload pack packages. + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); + + // Pack dependencies for preview components should be identical to the non-preview component. + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", previewComponentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", previewComponentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", previewComponentSwr); + + // Verify the SWIX authoring for the VS package wrapping the manifest MSI + string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "x64", "msi.swr")); + Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); + Assert.Contains("vs.package.type=msi", manifestMsiSwr); + Assert.Contains("vs.package.chip=x64", manifestMsiSwr); + Assert.DoesNotContain("vs.package.machineArch", manifestMsiSwr); + Assert.DoesNotContain("vs.package.outOfSupport", manifestMsiSwr); + + // Verify that no arm64 MSI authoring for VS. EMSDK doesn't define RIDs for arm64, but manifests always generate + // arm64 MSIs for the CLI based installs so we should not see that. + string swixRootDirectory = Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200"); + IEnumerable arm64Directories = Directory.EnumerateDirectories(swixRootDirectory, "arm64", SearchOption.AllDirectories); + Assert.DoesNotContain(arm64Directories, s => s.Contains("arm64")); + + // Verify the SWIX authoring for one of the workload pack MSIs. Packs get assigned random sub-folders so we + // need to filter out the SWIX project output items the task produced. + ITaskItem pythonPackSwixItem = createWorkloadTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.Emscripten.Python.6.0.4\x64")).FirstOrDefault(); + string packMsiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(pythonPackSwixItem.ItemSpec), "msi.swr")); + Assert.Contains("package name=Microsoft.Emscripten.Python.6.0.4", packMsiSwr); + Assert.Contains("vs.package.chip=x64", packMsiSwr); + Assert.Contains("vs.package.outOfSupport=yes", packMsiSwr); + Assert.DoesNotContain("vs.package.machineArch", packMsiSwr); + + // Verify the swix project items for components. The project files names always contain the major.minor suffix, so we'll end up + // with microsoft.net.sdk.emscripten.5.6.swixproj and microsoft.net.sdk.emscripten.pre.5.6.swixproj + IEnumerable swixComponentProjects = createWorkloadTask.SwixProjects.Where(s => s.GetMetadata(Metadata.PackageType).Equals(DefaultValues.PackageTypeComponent)); + Assert.All(swixComponentProjects, c => Assert.True(c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "true" || + !c.ItemSpec.Contains(".pre.") && c.GetMetadata(Metadata.IsPreview) == "false")); + } + + [WindowsOnlyFact] + public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio() + { + // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up + // conflicting sources from previous runs. + string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLa64"); + + if (Directory.Exists(baseIntermediateOutputPath)) + { + Directory.Delete(baseIntermediateOutputPath, recursive: true); + } + + ITaskItem[] manifestsPackages = new[] + { + new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) + .WithMetadata(Metadata.MsiVersion, "6.33.28") + .WithMetadata(Metadata.SupportsMachineArch, "true") + }; + + ITaskItem[] componentResources = new[] + { + new TaskItem("microsoft-net-sdk-emscripten") + .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") + .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") + .WithMetadata(Metadata.Version, "5.6.7.8") + }; + + ITaskItem[] shortNames = new[] + { + new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), + new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") + }; + + IBuildEngine buildEngine = new MockBuildEngine(); + + CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() + { + AllowMissingPacks = true, + BaseOutputPath = TestBase.BaseOutputPath, + BaseIntermediateOutputPath = baseIntermediateOutputPath, + BuildEngine = buildEngine, + ComponentResources = componentResources, + ManifestMsiVersion = null, + PackageSource = TestBase.TestAssetsPath, + ShortNames = shortNames, + WixToolsetPath = TestBase.WixToolsetPath, + WorkloadManifestPackageFiles = manifestsPackages, + }; + + bool result = createWorkloadTask.Execute(); + + Assert.True(result); + ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("d96ba8044ad35589f97716ecbf2732fb-arm64.msi")).FirstOrDefault(); + Assert.NotNull(manifestMsiItem); + + // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation. + // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band, + Assert.Equal("{CBA7CF4A-F3C9-3B75-8F1F-0D08AF7CD7BE}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode)); + // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would + // be generated. + Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion)); + Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,arm64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec)); + + // Process the template in the summary information stream. This is the only way to verify the intended platform + // of the MSI itself. + using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false); + Assert.Equal("Arm64;1033", si.Template); + + // Verify the SWIX authoring for the component representing the workload in VS. + string componentSwr = File.ReadAllText( + Path.Combine(Path.GetDirectoryName( + createWorkloadTask.SwixProjects.FirstOrDefault( + i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); + + // Emscripten is an abstract workload so it should be a component group. + Assert.Contains("vs.package.type=component", componentSwr); + Assert.Contains("isUiGroup=yes", componentSwr); + Assert.Contains("version=5.6.7.8", componentSwr); + // Default setting should be off + Assert.Contains("vs.package.outOfSupport=no", componentSwr); + + // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased + // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the + // aliased workload pack packages. + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); + + // Verify the SWIX authoring for the VS package wrapping the manifest MSI + string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "arm64", "msi.swr")); + Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); + Assert.Contains("vs.package.type=msi", manifestMsiSwr); + Assert.DoesNotContain("vs.package.chip", manifestMsiSwr); + Assert.Contains("vs.package.machineArch=arm64", manifestMsiSwr); + } + } +} From 9c7872bad70f2f958ee7ec030c928276e1138993 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 10 Feb 2026 17:11:30 -0800 Subject: [PATCH 22/26] cleanup, start removing HEAT dependencies --- .../EmscriptenTests.cs | 44 +++++++++---------- .../src/EmbeddedTemplates.cs | 1 + .../src/Msi/MsiTokens.cs | 10 +++++ .../src/Msi/WorkloadManifestMsi.wix.cs | 20 ++++++--- .../src/MsiTemplate/Files.wxs | 12 +++++ 5 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs index 25accdf44e8..6fb208ee4b0 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/EmscriptenTests.cs @@ -28,28 +28,28 @@ public static void ItCanCreateWorkloads() Directory.Delete(baseIntermediateOutputPath, recursive: true); } - ITaskItem[] manifestsPackages = new[] - { - new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) + ITaskItem[] manifestsPackages = + [ + new TaskItem(Path.Combine(TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) .WithMetadata(Metadata.MsiVersion, "6.33.28") - }; + ]; - ITaskItem[] componentResources = new[] - { + ITaskItem[] componentResources = + [ new TaskItem("microsoft-net-sdk-emscripten") .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") .WithMetadata(Metadata.Version, "5.6.7.8") - }; + ]; - ITaskItem[] shortNames = new[] - { + ITaskItem[] shortNames = + [ new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") - }; + ]; IBuildEngine buildEngine = new MockBuildEngine(); @@ -164,43 +164,43 @@ public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio() Directory.Delete(baseIntermediateOutputPath, recursive: true); } - ITaskItem[] manifestsPackages = new[] - { + ITaskItem[] manifestsPackages = + [ new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) .WithMetadata(Metadata.MsiVersion, "6.33.28") .WithMetadata(Metadata.SupportsMachineArch, "true") - }; + ]; - ITaskItem[] componentResources = new[] - { + ITaskItem[] componentResources = + [ new TaskItem("microsoft-net-sdk-emscripten") .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") .WithMetadata(Metadata.Version, "5.6.7.8") - }; + ]; - ITaskItem[] shortNames = new[] - { + ITaskItem[] shortNames = + [ new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") - }; + ]; IBuildEngine buildEngine = new MockBuildEngine(); CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() { AllowMissingPacks = true, - BaseOutputPath = TestBase.BaseOutputPath, + BaseOutputPath = BaseOutputPath, BaseIntermediateOutputPath = baseIntermediateOutputPath, BuildEngine = buildEngine, ComponentResources = componentResources, ManifestMsiVersion = null, - PackageSource = TestBase.TestAssetsPath, + PackageSource = TestAssetsPath, ShortNames = shortNames, - WixToolsetPath = TestBase.WixToolsetPath, + WixToolsetPath = WixToolsetPath, WorkloadManifestPackageFiles = manifestsPackages, }; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index c59dc98eb2c..524bb730bc5 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -71,6 +71,7 @@ static EmbeddedTemplates() { "Directories.wxs", $"{ns}.MsiTemplate.Directories.wxs" }, { "WorkloadPackDirectories.wxs", $"{ns}.MsiTemplate.WorkloadPackDirectories.wxs" }, { "dotnethome_x64.wxs", $"{ns}.MsiTemplate.dotnethome_x64.wxs" }, + { "Files.wxs",$"{ns}.MsiTemplate.Files.wxs" }, { "ManifestProduct.wxs", $"{ns}.MsiTemplate.ManifestProduct.wxs" }, { "WorkloadSetProduct.wxs", $"{ns}.MsiTemplate.WorkloadSetProduct.wxs" }, { "PackDirectories.wxs", $"{ns}.MsiTemplate.PackDirectories.wxs" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs index 91002bb059d..cbbc697955f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs @@ -11,5 +11,15 @@ internal class MsiTokens public static readonly string __DIR_REF_ID__ = nameof(__DIR_REF_ID__); public static readonly string __DIR_ID__ = nameof(__DIR_ID__); public static readonly string __DIR_NAME__ = nameof(__DIR_NAME__); + + /// + /// Replacement token for Files@Include. + /// + public static readonly string __INCLUDE__ = nameof(__INCLUDE__); + + /// + /// Replacement token for ComponentGroup@Id. + /// + public static readonly string __COMPONENT_GROUP_ID__ = nameof(__COMPONENT_GROUP_ID__); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 89a3b3f83c6..a420817f01e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; @@ -92,12 +93,21 @@ protected override WixProject CreateProject() EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); - // Configure harvesting of the manifest package contents. - string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); + // Configure file harvesting. string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data"); - wixproj.AddHarvestDirectory(packageDataDirectory, - AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, - PreprocessorDefinitionNames.SourceDir); + string filesWxs = EmbeddedTemplates.Extract("Files.wxs", WixSourceDirectory); + + Utils.StringReplace(filesWxs, Encoding.UTF8, + (MsiTokens.__DIR_ID__, AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory), + (MsiTokens.__COMPONENT_GROUP_ID__, "CG_PackageContents"), + (MsiTokens.__INCLUDE__, packageDataDirectory + Path.DirectorySeparatorChar + "**")); + + //// Configure harvesting of the manifest package contents. + //string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); + + //wixproj.AddHarvestDirectory(packageDataDirectory, + // AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + // PreprocessorDefinitionNames.SourceDir); foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f))) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs new file mode 100644 index 00000000000..e9a28b53842 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Files.wxs @@ -0,0 +1,12 @@ + + + + + + + + + + + From c3bb8143712da95b47e855c174e2beac2b37ae96 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Sat, 14 Feb 2026 00:58:38 -0800 Subject: [PATCH 23/26] Refactor manifest MSIs generation --- .../MsiTests.cs | 47 +++++++++++- .../src/DefaultValues.cs | 7 +- .../src/Msi/MsiBase.wix.cs | 73 ++++++++++++++++++- .../src/Msi/MsiTokens.cs | 5 ++ .../src/Msi/WorkloadManifestMsi.wix.cs | 33 ++------- .../src/Msi/WorkloadPackGroupMsi.wix.cs | 2 + .../src/Msi/WorkloadPackMsi.wix.cs | 2 + .../src/Msi/WorkloadSetMsi.wix.cs | 2 + .../src/MsiTemplate/ManifestProduct.wxs | 38 +++++++--- .../src/MsiTemplate/Registry.wxs | 2 +- 10 files changed, 164 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 2720f281462..4c4440d51da 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -154,9 +154,49 @@ public void ItCanBuildAManifestMsi() MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("ManifestVersionDir", "because the manifest MSI supports major upgrades"); + // Verify the installation record and dependency provider registry entries + var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); + string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); + string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"; + string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"; + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Name == "DependencyProviderKey" && + r.Value == "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"); + // The ProductCode is generated each time the MSI is built, but the value in the installation + // record should match the MSI's ProductCode property. + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductCode" && + string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase)); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "UpgradeCode" && + r.Value == "{e4761192-882d-38e9-a3f4-14b6c4ad12bd}"); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductVersion" && + r.Value == "1.2.3"); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductLanguage" && + r.Value == "#1033"); + registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && + r.Root == -1 && + r.Name == "Version" && + r.Value == "[ProductVersion]"); + registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && + r.Root == -1 && + r.Name == "DisplayName" && + r.Value == "[ProductName]"); + + // The files should contain the workload manifest and targets. + var files = MsiUtils.GetAllFiles(msiPath); + files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.json")); + files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.targets")); + // Verify that the wixpack archive was created. Assert.True(File.Exists(msi.GetMetadata(Metadata.Wixpack))); - } + } [WindowsOnlyFact] public void ItCanBuildATemplatePackMsi() @@ -189,8 +229,9 @@ public void ItCanBuildATemplatePackMsi() FileRow fileRow = MsiUtils.GetAllFiles(msiPath).FirstOrDefault(); Assert.Contains("microsoft.ios.templates.15.2.302-preview.14.122.nupkg", fileRow.FileName); - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().NotContain("PackageDir", "because it's a template pack"); - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); + var directories = MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory); + directories.Should().NotContain("PackageDir", "because it's a template pack"); + directories.Should().Contain("InstallDir", "because it's a workload pack"); } [WindowsOnlyFact] diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index 7467f6081e7..513978cc7b2 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -36,7 +36,12 @@ internal static class DefaultValues /// /// The default value to assign to the Manufacturer property of an MSI. /// - public static readonly string Manufacturer = "Microsoft Corporation"; + public const string Manufacturer = "Microsoft Corporation"; + + /// + /// Default Feature ID to reference when harvesting files. + /// + public const string PackageContentsFeatureId = "F_PackageContents"; public static readonly string x86 = "x64"; public static readonly string x64 = "x64"; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index 490f52cc8e1..cc479e52b40 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -7,9 +7,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; @@ -26,6 +28,11 @@ internal abstract class MsiBase /// private int _dirCount = 0; + /// + /// Used to track Files elements added for harvesting. + /// + private int _filesCount = 0; + /// /// The Arcade package that contains the CreateWixBuildWixpack task to support signing. /// @@ -52,6 +59,11 @@ internal abstract class MsiBase /// internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C"); + public abstract string ProductTemplate + { + get; + } + /// /// Metadata for the MSI such as package ID, version, author information, etc. /// @@ -258,7 +270,6 @@ protected string GenerateEula() /// Creates a basic WiX project using the specific toolset version and sets common properties and /// package references. /// - /// The WiX toolset version to use for building the project. /// An empty project. /// /// @@ -294,7 +305,7 @@ protected virtual WixProject CreateProject() // WiX only supports "full". If the property is overridden (Directory.build.props), // the compiler will report a warning, e.g. "warning WIX1098: The value 'embedded' is not a valid value for command line argument '-pdbType'. Using the value 'full' instead." wixproj.AddProperty(WixProperties.DebugType, "full"); - wixproj.AddProperty("IntermediateOutputPath", @"obj\\$(Configuration)"); + wixproj.AddProperty("IntermediateOutputPath", @"obj\$(Configuration)"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.Bitness, Platform == "x86" ? "always32" : "always64"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.EulaRtf, GenerateEula()); @@ -327,9 +338,41 @@ protected virtual WixProject CreateProject() // All workload MSIs (manifests or packs) need to override the default dialog set and select a minimal UI. wixproj.AddPackageReference(ToolsetPackages.MicrosoftWixToolsetUIExtension, WixToolsetPackageVersion); + // Extract common template source files used for all workload MSIs. + //EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); + //EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + EmbeddedTemplates.Extract(ProductTemplate, WixSourceDirectory, "Product.wxs"); + return wixproj; } + /// + /// Adds a set of files to be harvested for inclusion in the MSI package. The harvested files + /// will be placed in a component group and added to the specified feature. + /// + /// The directory ID containing the directory path for the root of the harvested files. + /// The directory to include for harvesting. + /// Globbing pattern to use for harvesting. The default is "**", indicating that directories should be recursed. + /// The ID of the feature to add the generated component group holding the harvested files. + protected void AddFiles(string dirId, string include, string wildcard = "**", string featureId = DefaultValues.PackageContentsFeatureId) + { + // Generate sequential templates, e.g., Files00.wxs, Files01.wxs, etc. + string idSuffix = $"{_filesCount:D2}"; + string componentGroupId = $"CG_{idSuffix}"; + string filesWxs = EmbeddedTemplates.Extract("Files.wxs", WixSourceDirectory, $"Files{idSuffix}.wxs"); + + Utils.StringReplace(filesWxs, Encoding.UTF8, + (MsiTokens.__COMPONENT_GROUP_ID__, componentGroupId), + (MsiTokens.__DIR_ID__, dirId), + (MsiTokens.__INCLUDE__, include + Path.DirectorySeparatorChar + wildcard)); + + AddComponentGroupReferenceToFeature(featureId, componentGroupId); + + _filesCount++; + } + /// /// Builds the MSI and returns a task item with metadata about the MSI. /// @@ -350,7 +393,7 @@ public virtual ITaskItem Build(string outputPath) Encoding.UTF8, ("__WIXPACK_OUTPUT_DIR__", WixpackOutputDirectory)); // Add a package reference to pull in the CreateWixBuildWixpack task. The version - // should automatically default to the "major.minor.path-*", e.g. 10.0.0-* + // should automatically default to the "major.minor.patch-*", e.g. 10.0.0-* wixproj.AddPackageReference(_MicrosoftDotNetBuildTaskInstallers, ToolsetInfo.ArcadeVersion); wixproj.AddProperty(WixProperties.GenerateWixpack, "true"); @@ -440,6 +483,30 @@ protected void AddDirectory(string name, string id, string reference) } } + /// + /// Adds a reference to a component group within a specified feature in the Product.wxs file. + /// + /// If the specified feature is not found in the Product.wxs file, no changes are made. + /// This method updates the Product.wxs file in the directory specified by WixSourceDirectory. + /// The identifier of the feature to which the component group reference will be added. Must match the value of + /// the 'Id' attribute of an existing element. + /// The identifier of the component group to reference. This value is set as the 'Id' attribute of the new + /// element. + protected void AddComponentGroupReferenceToFeature(string featureId, string componentGroupId) + { + var productDoc = XDocument.Load(Path.Combine(WixSourceDirectory, "Product.wxs")); + var ns = productDoc.Root!.Name.Namespace; + + var featureElement = productDoc.Root.Descendants(ns + "Feature") + .FirstOrDefault(f => f.Attribute("Id")?.Value == featureId); + + if (featureElement != null) + { + featureElement.Add(new XElement(ns + "ComponentGroupRef", new XAttribute("Id", componentGroupId))); + productDoc.Save(Path.Combine(WixSourceDirectory, "Product.wxs")); + } + } + /// /// Creates a source file containing a directory fragment. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs index cbbc697955f..d69df353466 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiTokens.cs @@ -21,5 +21,10 @@ internal class MsiTokens /// Replacement token for ComponentGroup@Id. /// public static readonly string __COMPONENT_GROUP_ID__ = nameof(__COMPONENT_GROUP_ID__); + + /// + /// Replacement token for FeatureRef@Id. + /// + public static readonly string __FEATURE_REF_ID__ = nameof(__FEATURE_REF_ID__); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index a420817f01e..6f3201359a0 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -20,6 +20,8 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi /// internal class WorkloadManifestMsi : MsiBase { + public override string ProductTemplate => "ManifestProduct.wxs"; + public WorkloadManifestPackage Package { get; } public List WorkloadPackGroups { get; } = new(); @@ -86,28 +88,10 @@ protected override WixProject CreateProject() { WixProject wixproj = base.CreateProject(); - // Add source files - EmbeddedTemplates.Extract("ManifestProduct.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); - // Configure file harvesting. string packageDataDirectory = Path.Combine(Package.DestinationDirectory, "data"); - string filesWxs = EmbeddedTemplates.Extract("Files.wxs", WixSourceDirectory); - - Utils.StringReplace(filesWxs, Encoding.UTF8, - (MsiTokens.__DIR_ID__, AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory), - (MsiTokens.__COMPONENT_GROUP_ID__, "CG_PackageContents"), - (MsiTokens.__INCLUDE__, packageDataDirectory + Path.DirectorySeparatorChar + "**")); - - //// Configure harvesting of the manifest package contents. - //string wixProjectPath = Path.Combine(WixSourceDirectory, "manifest.wixproj"); - - //wixproj.AddHarvestDirectory(packageDataDirectory, - // AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, - // PreprocessorDefinitionNames.SourceDir); + AddFiles(AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + packageDataDirectory); foreach (var file in Directory.GetFiles(packageDataDirectory).Select(f => Path.GetFullPath(f))) { @@ -115,7 +99,6 @@ protected override WixProject CreateProject() } // Add WorkloadPackGroups.json to add to workload manifest MSI - string? jsonContentWxs = null; string? jsonDirectory = null; // Default the variable to false. If we harvested workload pack group data, we'll override it @@ -123,8 +106,6 @@ protected override WixProject CreateProject() 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); @@ -132,10 +113,8 @@ protected override WixProject CreateProject() string jsonFullPath = Path.GetFullPath(Path.Combine(jsonDirectory, "WorkloadPackGroups.json")); File.WriteAllText(jsonFullPath, jsonAsString); - wixproj.AddHarvestDirectory(jsonDirectory, - AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, - "JsonSourceDir", - "CG_PackGroupJson"); + AddFiles(AllowSideBySideInstalls ? MsiDirectories.ManifestVersionDirectory : MsiDirectories.ManifestIdDirectory, + jsonDirectory); wixproj.AddPreprocessorDefinition("IncludePackGroupJson", "true"); wixproj.AddPreprocessorDefinition("JsonSourceDir", jsonDirectory); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs index 9573de7d24c..f3c563ac3b8 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackGroupMsi.wix.cs @@ -24,6 +24,8 @@ internal class WorkloadPackGroupMsi : MsiBase /// protected override string BaseOutputName => Metadata.Id; + public override string ProductTemplate => "Product.wxs"; + protected override string ProviderKeyName => $"{_package.Id},{Metadata.PackageVersion},{Platform}"; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index 23e6aeda3ba..b6ccf2dd0c7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -18,6 +18,8 @@ internal class WorkloadPackMsi : MsiBase /// protected override string BaseOutputName => _package.ShortName; + public override string ProductTemplate => "Product.wxs"; + protected override string ProviderKeyName => $"{_package.Id},{_package.PackageVersion},{Platform}"; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs index 716689be2f9..f1e6f8b61cc 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -17,6 +17,8 @@ internal class WorkloadSetMsi : MsiBase { private WorkloadSetPackage _package; + public override string ProductTemplate => "WorkloadSetProduct.wxs"; + protected override string BaseOutputName => Path.GetFileNameWithoutExtension(_package.PackagePath); protected override string ProviderKeyName => 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 eb1ccc8074a..1519c1c473c 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -1,7 +1,8 @@ - + @@ -22,18 +23,31 @@ Message="A newer version of [ProductName] is alread installed." /> - - - - - - - - + + + + + + + + + + + + + + + - - + + + + + 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 a8bbfc9974c..c48feff2509 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs @@ -4,7 +4,7 @@ - + From 5aee93cce102b14e0034e13bae7014e9fdc06ddb Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 17 Feb 2026 16:26:22 -0800 Subject: [PATCH 24/26] More refactoring --- .../MsiTests.cs | 128 +++++++++++------- .../src/Msi/MsiBase.wix.cs | 28 +++- .../src/Msi/WorkloadPackMsi.wix.cs | 26 ++-- .../src/MsiTemplate/Directories.wxs | 2 +- .../src/MsiTemplate/Product.wxs | 28 +++- 5 files changed, 140 insertions(+), 72 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 4c4440d51da..5f4186f061e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -11,6 +11,7 @@ using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.NET.Sdk.WorkloadManifestReader; +using Microsoft.Win32; using WixToolset.Dtf.WindowsInstaller; using Xunit; using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; @@ -20,6 +21,47 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests [Collection("MSI tests")] public class MsiTests : TestBase { + private static void ValidateInstallationRecord(IEnumerable registryKeys, + string installationRecordKeyName, string expectedProviderKey, string expectedProductCode, string expectedUpgradeCode, + string expectedProductVersion, + string expectedProductLanguage = "#1033") + { + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "DependencyProviderKey" && + r.Value == expectedProviderKey); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductCode" && + string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase)); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "UpgradeCode" && + string.Equals(r.Value, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase)); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductVersion" && + r.Value == expectedProductVersion); + registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && + r.Root == 2 && + r.Name == "ProductLanguage" && + r.Value == expectedProductLanguage); + } + + private static void ValidateDependencyProviderKey(IEnumerable registryKeys, string dependencyProviderKeyName) + { + // Dependency provider entries references the ProductVersion and ProductName properties. These + // properties are set by the installer service at install time. + registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && + r.Root == -1 && + r.Name == "Version" && + r.Value == "[ProductVersion]"); + registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && + r.Root == -1 && + r.Name == "DisplayName" && + r.Value == "[ProductName]"); + } + /// /// Helper method for generating workload manifest MSIs. /// @@ -44,16 +86,6 @@ private static ITaskItem BuildManifestMsi(string outputDirectory, string package return msi.Build(Path.Combine(outputDirectory, "msi")); } - [WindowsOnlyFact] - public void WorkloadManifestsIncludeInstallationRecords() - { - ITaskItem msi603 = BuildManifestMsi(GetTestCaseDirectory(), Path.Combine(TestAssetsPath, "microsoft.net.workload.mono.toolchain.manifest-6.0.200.6.0.3.nupkg")); - string msiPath603 = msi603.GetMetadata(Metadata.FullPath); - - MsiUtils.GetAllRegistryKeys(msiPath603).Should().Contain(r => - r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"); - } - [WindowsOnlyFact] public void ItCanBuildWorkloadSdkPackMsi() { @@ -65,7 +97,9 @@ public void ItCanBuildWorkloadSdkPackMsi() WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, new Version("1.2.3")); // Parse the manifest to extract information related to workload packs so we can extract a specific pack. WorkloadManifest manifest = manifestPackage.GetManifest(); - WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Sdk"); + //WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Sdk"); + WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Python"); + // Microsoft.NET.Runtime.Emscripten.Python WorkloadPack pack = manifest.Packs[packId]; var sourcePackages = WorkloadPackPackage.GetSourcePackages(TestAssetsPath, pack); @@ -79,10 +113,6 @@ public void ItCanBuildWorkloadSdkPackMsi() var msi = workloadPackMsi.Build(msiOutputDirectory); string msiPath = msi.GetMetadata(Metadata.FullPath); - // Verify workload record - MsiUtils.GetAllRegistryKeys(msiPath).Should().Contain(r => - r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Sdk.win-x64\6.0.4"); - // Process the summary information stream's template to extract the MSIs target platform. using SummaryInfo si = new(msiPath, enableWrite: false); Assert.Equal("x64;1033", si.Template); @@ -92,7 +122,19 @@ public void ItCanBuildWorkloadSdkPackMsi() MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). - Assert.Equal("{A06E6854-C6B0-3C8D-8D0C-F0704755303B}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); + string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode); + Assert.Equal("{BDE8712D-9BD7-3692-9C2A-C518208967D6}", upgradeCode); + + // Verify the installation record and dependency provider registry entries + var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); + string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); + string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64\6.0.4"; + string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64"; + + ValidateInstallationRecord(registryKeys, installationRecordKeyName, + "Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64", + expectedProductCode, upgradeCode, "6.0.4.0"); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); } [WindowsOnlyFact] @@ -145,7 +187,8 @@ public void ItCanBuildAManifestMsi() using SummaryInfo si = new(msiPath, enableWrite: false); // UpgradeCode is predictable/stable for manifest MSIs that support major upgrades. - Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); + string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode); + Assert.Equal("{E4761192-882D-38E9-A3F4-14B6C4AD12BD}", upgradeCode); Assert.Equal("1.2.3", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion)); Assert.Equal("Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", MsiUtils.GetProviderKeyName(msiPath)); Assert.Equal("x64;1033", si.Template); @@ -159,37 +202,15 @@ public void ItCanBuildAManifestMsi() string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"; string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"; - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Name == "DependencyProviderKey" && - r.Value == "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"); - // The ProductCode is generated each time the MSI is built, but the value in the installation - // record should match the MSI's ProductCode property. - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductCode" && - string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase)); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "UpgradeCode" && - r.Value == "{e4761192-882d-38e9-a3f4-14b6c4ad12bd}"); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductVersion" && - r.Value == "1.2.3"); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductLanguage" && - r.Value == "#1033"); - registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && - r.Root == -1 && - r.Name == "Version" && - r.Value == "[ProductVersion]"); - registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && - r.Root == -1 && - r.Name == "DisplayName" && - r.Value == "[ProductName]"); - // The files should contain the workload manifest and targets. + ValidateInstallationRecord(registryKeys, installationRecordKeyName, + "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", + expectedProductCode, upgradeCode, "1.2.3"); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); + + // The File table should contain the workload manifest and targets. There may be additional + // localized content for the manifests. Their presence is neither required nor critical to + // how workloads functions. var files = MsiUtils.GetAllFiles(msiPath); files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.json")); files.Should().Contain(f => f.FileName.EndsWith("WorkloadManifest.targets")); @@ -218,7 +239,8 @@ public void ItCanBuildATemplatePackMsi() using SummaryInfo si = new(msiPath, enableWrite: false); // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). - Assert.Equal("{EC4D6B34-C9DE-3984-97FD-B7AC96FA536A}", MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode)); + string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode); + Assert.Equal("{EC4D6B34-C9DE-3984-97FD-B7AC96FA536A}", upgradeCode); // The version is set using the package major.minor.patch Assert.Equal("15.2.302.0", MsiUtils.GetProperty(msiPath, MsiProperty.ProductVersion)); Assert.Equal("Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", MsiUtils.GetProviderKeyName(msiPath)); @@ -232,6 +254,16 @@ public void ItCanBuildATemplatePackMsi() var directories = MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory); directories.Should().NotContain("PackageDir", "because it's a template pack"); directories.Should().Contain("InstallDir", "because it's a workload pack"); + + // Verify the installation record and dependency provider registry entries + var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); + string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); + string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.iOS.Templates\15.2.302-preview.14.122"; + string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.iOS.Templates,15.2.302-preview.14.122,x64"; + + ValidateInstallationRecord(registryKeys, installationRecordKeyName, + "Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", expectedProductCode, upgradeCode, "15.2.302.0"); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); } [WindowsOnlyFact] diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs index cc479e52b40..20e9d415980 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs @@ -349,8 +349,7 @@ protected virtual WixProject CreateProject() } /// - /// Adds a set of files to be harvested for inclusion in the MSI package. The harvested files - /// will be placed in a component group and added to the specified feature. + /// Adds a Files element to harvest files and place them in the specified component group and feature. /// /// The directory ID containing the directory path for the root of the harvested files. /// The directory to include for harvesting. @@ -373,6 +372,29 @@ protected void AddFiles(string dirId, string include, string wildcard = "**", st _filesCount++; } + /// + /// Creates a new Directory element using the specified ID and name under the specified parent directory. + /// + /// The identifier of the directory. + /// The name of the directory. + /// The identifier of the parent directory. + /// The source file used for adding the directory and searching for parent references. + protected void AddDirectory(string id, string name, string parentId = "DOTNETHOME", string sourceFile = "Directories.wxs") + { + var srcPath = Path.Combine(WixSourceDirectory, sourceFile); + var productDoc = XDocument.Load(srcPath); + var ns = productDoc.Root!.Name.Namespace; + + var parentDirectoryElement = productDoc.Root.Descendants(ns + "Directory") + .FirstOrDefault(f => f.Attribute("Id")?.Value == parentId); + + if (parentDirectoryElement != null) + { + parentDirectoryElement.Add(new XElement(ns + "Directory", new XAttribute("Id", id), new XAttribute("Name", name))); + productDoc.Save(srcPath); + } + } + /// /// Builds the MSI and returns a task item with metadata about the MSI. /// @@ -471,7 +493,7 @@ protected void AddDefaultPackageFiles(ITaskItem msi) /// The ID of the directory. /// The ID of the directory reference (parent directory). - protected void AddDirectory(string name, string id, string reference) + protected void AddDirectory2(string name, string id, string reference) { try { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs index b6ccf2dd0c7..2c8eb9bb412 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadPackMsi.wix.cs @@ -43,24 +43,20 @@ public WorkloadPackMsi(WorkloadPackPackage package, string platform, IBuildEngin protected override WixProject CreateProject() { WixProject wixproj = base.CreateProject(); - string wixProjectPath = Path.Combine(WixSourceDirectory, "pack.wixproj"); - EmbeddedTemplates.Extract("Product.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("WorkloadPackDirectories.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("Registry.wxs", WixSourceDirectory); + // Add the default installation directory based on the workload pack kind. + AddDirectory("InstallDir", GetInstallDir(_package.Kind)); + string directoryReference = "InstallDir"; - string directoryReference = _package.Kind == WorkloadPackKind.Library || _package.Kind == WorkloadPackKind.Template ? - "InstallDir" : "VersionDir"; - - wixproj.AddHarvestDirectory(_package.DestinationDirectory, directoryReference, - PreprocessorDefinitionNames.SourceDir); + if (_package.Kind != WorkloadPackKind.Library && _package.Kind != WorkloadPackKind.Template) + { + AddDirectory("PackageDir", Metadata.Id, "InstallDir"); + AddDirectory("VersionDir", Metadata.PackageVersion.ToString(), "PackageDir"); + // Override the directory refernece for harvesting. + directoryReference = "VersionDir"; + } - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.InstallDir, $"{GetInstallDir(_package.Kind)}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.PackKind, $"{_package.Kind}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{_package.DestinationDirectory}"); + AddFiles(directoryReference, _package.DestinationDirectory); return wixproj; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs index 06a0014520a..95f04e188a8 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -12,7 +12,7 @@ - + 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 a9d42928dde..05ba52ad7ee 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Product.wxs @@ -1,5 +1,6 @@ - + @@ -10,12 +11,29 @@ - - + + + + + + + + + + + + + - - + + + + + From 2737c5bd5d8cbcd6e130920bb88df95c8232ba38 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Tue, 17 Feb 2026 17:07:11 -0800 Subject: [PATCH 25/26] Simplify directories for manifest installers --- .../src/Msi/WorkloadManifestMsi.wix.cs | 13 ++++----- .../src/MsiTemplate/Directories.wxs | 27 ------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs index 6f3201359a0..311f744a379 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadManifestMsi.wix.cs @@ -7,8 +7,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json; using System.Text; +using System.Text.Json; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; @@ -122,17 +123,17 @@ protected override WixProject CreateProject() NuGetPackageFiles[jsonFullPath] = @"\data\extractedManifest\" + Path.GetFileName(jsonFullPath); } - // Add preprocessor definitions - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{Package.SdkFeatureBand}"); + // Add manifest directories. + AddDirectory("SdkManifestDir", "sdk-manifests"); + AddDirectory("SdkFeatureBandVersionDir", $"{Package.SdkFeatureBand}", "SdkManifestDir"); // The temporary installer in the SDK (6.0) used lower invariants of the manifest ID. // We have to do the same to ensure the keypath generation produces stable GUIDs. - wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.ManifestId, $"{Package.ManifestId.ToLowerInvariant()}"); + AddDirectory("ManifestIdDir", $"{Package.ManifestId.ToLowerInvariant()}", "SdkFeatureBandVersionDir"); if (AllowSideBySideInstalls) { - wixproj.AddPreprocessorDefinition("ManifestVersion", Package.GetManifest().Version); + AddDirectory("ManifestVersionDir", Package.GetManifest().Version, "ManifestIdDir"); } return wixproj; diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs index 95f04e188a8..0ba3dadc0b4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -14,16 +14,6 @@ - - - - - - - - - - @@ -33,21 +23,4 @@ - - - - - - - - - - - - - - - - - From ba76d91277152a74f33ebb075ac180abc53b7a37 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Wed, 18 Feb 2026 00:13:16 -0800 Subject: [PATCH 26/26] Update workload sets --- .../CreateVisualStudioWorkloadSetTests.cs | 56 ++++++++++---- .../MsiTests.cs | 75 ++++--------------- .../TestBase.cs | 45 ++++++++++- .../src/Msi/WorkloadSetMsi.wix.cs | 20 ++--- .../src/MsiTemplate/Directories.wxs | 10 --- .../src/MsiTemplate/Registry.wxs | 2 +- .../src/MsiTemplate/WorkloadSetProduct.wxs | 16 +++- 7 files changed, 126 insertions(+), 98 deletions(-) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs index 35bb09fc533..e5e64f77ac3 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadSetTests.cs @@ -8,6 +8,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using WixToolset.Dtf.WindowsInstaller; using Xunit; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests @@ -48,35 +49,62 @@ public void ItCanCreateWorkloadSets() Assert.True(createWorkloadSetTask.Execute(), buildEngine.BuildErrorEvents.Count > 0 ? buildEngine.BuildErrorEvents[0].Message : "Task failed. No error events"); - // Spot check the x64 generated MSI. - ITaskItem msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "x64"); - Assert.NotNull(msi); + // Validate the arm64 installer. + ITaskItem arm64Msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "arm64"); + Assert.NotNull(arm64Msi); + ITaskItem x64Msi = createWorkloadSetTask.Msis.FirstOrDefault(i => i.GetMetadata(Metadata.Platform) == "x64"); + Assert.NotNull(x64Msi); - // Verify the workload set records the CLI will use. - MsiUtils.GetAllRegistryKeys(msi.ItemSpec).Should().Contain(r => - r.Root == 2 && - r.Key == @"SOFTWARE\Microsoft\dotnet\InstalledWorkloadSets\x64\9.0.100\9.0.100-baseline.1.23464.1" && - r.Name == "ProductVersion" && - r.Value == "12.8.45"); + var arm64MsiPath = arm64Msi.ItemSpec; + var x64MsiPath = x64Msi.ItemSpec; + + // Process the summary information stream's template to extract the MSIs target platform. + using SummaryInfo si = new(arm64MsiPath, enableWrite: false); + Assert.Equal("Arm64;1033", si.Template); + + // Upgrades are not supported, but we do generated stable GUIDs based on various + // properties including the target platform. + string upgradeCode = MsiUtils.GetProperty(arm64MsiPath, MsiProperty.UpgradeCode); + Assert.Equal("{A05B88DE-F40F-3C20-B6DA-719B8EED1D9F}", upgradeCode); + // Make sure the x64 and arm64 MSIs have different UpgradeCode properties. + string x64UpgradeCode = MsiUtils.GetProperty(x64MsiPath, MsiProperty.UpgradeCode); + Assert.NotEqual(upgradeCode, x64UpgradeCode); + + // Verify the installation record and dependency provider registry entries. + var registryKeys = MsiUtils.GetAllRegistryKeys(arm64MsiPath); + string productCode = MsiUtils.GetProperty(arm64MsiPath, MsiProperty.ProductCode); + string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledWorkloadSets\arm64\9.0.100\9.0.100-baseline.1.23464.1"; + string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Set,9.0.100,9.0.100-baseline.1.23464.1,arm64"; + + // ProductCode and UpgradeCode values in the installation record should match the + // values from the Property table. + ValidateInstallationRecord(registryKeys, installationRecordKey, + "Microsoft.NET.Workload.Set,9.0.100,9.0.100-baseline.1.23464.1,arm64", + productCode, upgradeCode, "12.8.45"); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKey); // Workload sets are SxS. Verify that we don't have an Upgrade table. // This requires suppressing the default behavior by setting Package@UpgradeStrategy to "none". - MsiUtils.HasTable(msi.ItemSpec, "Upgrade").Should().BeFalse("because workload sets are side-by-side"); + MsiUtils.HasTable(arm64MsiPath, "Upgrade").Should().BeFalse("because workload sets are side-by-side"); // Verify the workloadset version directory and only look at the long name version. - DirectoryRow versionDir = MsiUtils.GetAllDirectories(msi.ItemSpec).FirstOrDefault(d => string.Equals(d.Directory, "WorkloadSetVersionDir")); + DirectoryRow versionDir = MsiUtils.GetAllDirectories(arm64MsiPath).FirstOrDefault(d => string.Equals(d.Directory, "WorkloadSetVersionDir")); Assert.NotNull(versionDir); Assert.Contains("|9.0.0.100-baseline.1.23464.1", versionDir.DefaultDir); + // Verify that the workloadset.json exists. + var files = MsiUtils.GetAllFiles(arm64MsiPath); + files.Should().Contain(f => f.FileName.EndsWith("|workloadset.json")); + // Verify the SWIX authoring for one of the workload set MSIs. - ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\x64")).FirstOrDefault(); + ITaskItem workloadSetSwixItem = createWorkloadSetTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1\arm64")).FirstOrDefault(); Assert.Equal(DefaultValues.PackageTypeMsiWorkloadSet, workloadSetSwixItem.GetMetadata(Metadata.PackageType)); string msiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(workloadSetSwixItem.ItemSpec), "msi.swr")); Assert.Contains("package name=Microsoft.NET.Workloads.9.0.100.9.0.100-baseline.1.23464.1", msiSwr); Assert.Contains("version=12.8.45", msiSwr); - Assert.DoesNotContain("vs.package.chip=x64", msiSwr); - Assert.Contains("vs.package.machineArch=x64", msiSwr); + Assert.DoesNotContain("vs.package.chip=arm64", msiSwr); + Assert.Contains("vs.package.machineArch=arm64", msiSwr); Assert.Contains("vs.package.type=msi", msiSwr); // Verify package group SWIX project diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs index 5f4186f061e..d3770320264 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/MsiTests.cs @@ -11,7 +11,6 @@ using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.NET.Sdk.WorkloadManifestReader; -using Microsoft.Win32; using WixToolset.Dtf.WindowsInstaller; using Xunit; using static Microsoft.DotNet.Build.Tasks.Workloads.Msi.WorkloadManifestMsi; @@ -21,47 +20,6 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests [Collection("MSI tests")] public class MsiTests : TestBase { - private static void ValidateInstallationRecord(IEnumerable registryKeys, - string installationRecordKeyName, string expectedProviderKey, string expectedProductCode, string expectedUpgradeCode, - string expectedProductVersion, - string expectedProductLanguage = "#1033") - { - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "DependencyProviderKey" && - r.Value == expectedProviderKey); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductCode" && - string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase)); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "UpgradeCode" && - string.Equals(r.Value, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase)); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductVersion" && - r.Value == expectedProductVersion); - registryKeys.Should().Contain(r => r.Key == installationRecordKeyName && - r.Root == 2 && - r.Name == "ProductLanguage" && - r.Value == expectedProductLanguage); - } - - private static void ValidateDependencyProviderKey(IEnumerable registryKeys, string dependencyProviderKeyName) - { - // Dependency provider entries references the ProductVersion and ProductName properties. These - // properties are set by the installer service at install time. - registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && - r.Root == -1 && - r.Name == "Version" && - r.Value == "[ProductVersion]"); - registryKeys.Should().Contain(r => r.Key == dependencyProviderKeyName && - r.Root == -1 && - r.Name == "DisplayName" && - r.Value == "[ProductName]"); - } - /// /// Helper method for generating workload manifest MSIs. /// @@ -97,9 +55,7 @@ public void ItCanBuildWorkloadSdkPackMsi() WorkloadManifestPackage manifestPackage = new(packageItem, packageContentsDirectory, new Version("1.2.3")); // Parse the manifest to extract information related to workload packs so we can extract a specific pack. WorkloadManifest manifest = manifestPackage.GetManifest(); - //WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Sdk"); WorkloadPackId packId = new("Microsoft.NET.Runtime.Emscripten.Python"); - // Microsoft.NET.Runtime.Emscripten.Python WorkloadPack pack = manifest.Packs[packId]; var sourcePackages = WorkloadPackPackage.GetSourcePackages(TestAssetsPath, pack); @@ -118,8 +74,9 @@ public void ItCanBuildWorkloadSdkPackMsi() Assert.Equal("x64;1033", si.Template); // Verify pack directories - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("PackageDir", "because it's an SDK pack"); - MsiUtils.GetAllDirectories(msiPath).Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); + var directories = MsiUtils.GetAllDirectories(msiPath); + directories.Select(d => d.Directory).Should().Contain("PackageDir", "because it's an SDK pack"); + directories.Select(d => d.Directory).Should().Contain("InstallDir", "because it's a workload pack"); // UpgradeCode is predictable/stable for pack MSIs since they are seeded using the package identity (ID & version). string upgradeCode = MsiUtils.GetProperty(msiPath, MsiProperty.UpgradeCode); @@ -128,13 +85,13 @@ public void ItCanBuildWorkloadSdkPackMsi() // Verify the installation record and dependency provider registry entries var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); - string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64\6.0.4"; - string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64"; + string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64\6.0.4"; + string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64"; - ValidateInstallationRecord(registryKeys, installationRecordKeyName, + ValidateInstallationRecord(registryKeys, installationRecordKey, "Microsoft.NET.Runtime.Emscripten.2.0.23.Python.win-x64,6.0.4,x64", expectedProductCode, upgradeCode, "6.0.4.0"); - ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKey); } [WindowsOnlyFact] @@ -200,13 +157,13 @@ public void ItCanBuildAManifestMsi() // Verify the installation record and dependency provider registry entries var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); - string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"; - string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"; + string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledManifests\x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.200\6.0.3"; + string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64"; - ValidateInstallationRecord(registryKeys, installationRecordKeyName, + ValidateInstallationRecord(registryKeys, installationRecordKey, "Microsoft.NET.Workload.Mono.ToolChain,6.0.200,x64", expectedProductCode, upgradeCode, "1.2.3"); - ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKey); // The File table should contain the workload manifest and targets. There may be additional // localized content for the manifests. Their presence is neither required nor critical to @@ -217,7 +174,7 @@ public void ItCanBuildAManifestMsi() // Verify that the wixpack archive was created. Assert.True(File.Exists(msi.GetMetadata(Metadata.Wixpack))); - } + } [WindowsOnlyFact] public void ItCanBuildATemplatePackMsi() @@ -258,12 +215,12 @@ public void ItCanBuildATemplatePackMsi() // Verify the installation record and dependency provider registry entries var registryKeys = MsiUtils.GetAllRegistryKeys(msiPath); string expectedProductCode = MsiUtils.GetProperty(msiPath, MsiProperty.ProductCode); - string installationRecordKeyName = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.iOS.Templates\15.2.302-preview.14.122"; - string dependencyProviderKeyName = @"Software\Classes\Installer\Dependencies\Microsoft.iOS.Templates,15.2.302-preview.14.122,x64"; + string installationRecordKey = @"SOFTWARE\Microsoft\dotnet\InstalledPacks\x64\Microsoft.iOS.Templates\15.2.302-preview.14.122"; + string dependencyProviderKey = @"Software\Classes\Installer\Dependencies\Microsoft.iOS.Templates,15.2.302-preview.14.122,x64"; - ValidateInstallationRecord(registryKeys, installationRecordKeyName, + ValidateInstallationRecord(registryKeys, installationRecordKey, "Microsoft.iOS.Templates,15.2.302-preview.14.122,x64", expectedProductCode, upgradeCode, "15.2.302.0"); - ValidateDependencyProviderKey(registryKeys, dependencyProviderKeyName); + ValidateDependencyProviderKey(registryKeys, dependencyProviderKey); } [WindowsOnlyFact] diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs index 241e3a30fef..278027c4887 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/TestBase.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; +using Microsoft.Arcade.Test.Common; using Microsoft.Build.Utilities; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; -using Microsoft.Arcade.Test.Common; +using FluentAssertions; namespace Microsoft.DotNet.Build.Tasks.Workloads.Tests { @@ -35,5 +37,46 @@ public abstract class TestBase /// public string GetTestCaseDirectory() => Path.Combine(TestOutputRoot, Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); + + protected static void ValidateInstallationRecord(IEnumerable registryKeys, + string installationRecordKey, string expectedProviderKey, string expectedProductCode, string expectedUpgradeCode, + string expectedProductVersion, + string expectedProductLanguage = "#1033") + { + registryKeys.Should().Contain(r => r.Key == installationRecordKey && + r.Root == 2 && + r.Name == "DependencyProviderKey" && + r.Value == expectedProviderKey); + registryKeys.Should().Contain(r => r.Key == installationRecordKey && + r.Root == 2 && + r.Name == "ProductCode" && + string.Equals(r.Value, expectedProductCode, StringComparison.OrdinalIgnoreCase)); + registryKeys.Should().Contain(r => r.Key == installationRecordKey && + r.Root == 2 && + r.Name == "UpgradeCode" && + string.Equals(r.Value, expectedUpgradeCode, StringComparison.OrdinalIgnoreCase)); + registryKeys.Should().Contain(r => r.Key == installationRecordKey && + r.Root == 2 && + r.Name == "ProductVersion" && + r.Value == expectedProductVersion); + registryKeys.Should().Contain(r => r.Key == installationRecordKey && + r.Root == 2 && + r.Name == "ProductLanguage" && + r.Value == expectedProductLanguage); + } + + protected static void ValidateDependencyProviderKey(IEnumerable registryKeys, string dependencyProviderKey) + { + // Dependency provider entries references the ProductVersion and ProductName properties. These + // properties are set by the installer service at install time. + registryKeys.Should().Contain(r => r.Key == dependencyProviderKey && + r.Root == -1 && + r.Name == "Version" && + r.Value == "[ProductVersion]"); + registryKeys.Should().Contain(r => r.Key == dependencyProviderKey && + r.Root == -1 && + r.Name == "DisplayName" && + r.Value == "[ProductName]"); + } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs index f1e6f8b61cc..08c27b981ad 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/WorkloadSetMsi.wix.cs @@ -6,8 +6,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Packaging; using System.Linq; using System.Text.Json; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.DotNet.Build.Tasks.Workloads.Wix; @@ -21,12 +23,12 @@ internal class WorkloadSetMsi : MsiBase protected override string BaseOutputName => Path.GetFileNameWithoutExtension(_package.PackagePath); - protected override string ProviderKeyName => + protected override string ProviderKeyName => $"Microsoft.NET.Workload.Set,{_package.SdkFeatureBand},{_package.PackageVersion},{Platform}"; protected override string? InstallationRecordKey => "InstalledWorkloadSets"; - protected override Guid UpgradeCode => + protected override Guid UpgradeCode => Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{_package.Identity};{Platform}"); protected override string? MsiPackageType => DefaultValues.WorkloadSetMsi; @@ -44,14 +46,14 @@ protected override WixProject CreateProject() { WixProject wixproj = base.CreateProject(); - EmbeddedTemplates.Extract("DependencyProvider.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("Directories.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("dotnethome_x64.wxs", WixSourceDirectory); - EmbeddedTemplates.Extract("WorkloadSetProduct.wxs", WixSourceDirectory); - string packageDataDirectory = Path.Combine(_package.DestinationDirectory, "data"); - wixproj.AddHarvestDirectory(packageDataDirectory, MsiDirectories.WorkloadSetVersionDirectory, - PreprocessorDefinitionNames.SourceDir); + + AddFiles(MsiDirectories.WorkloadSetVersionDirectory, packageDataDirectory); + + AddDirectory("SdkManifestDir", "sdk-manifests"); + AddDirectory("SdkFeatureBandVersionDir", $"{_package.SdkFeatureBand}", "SdkManifestDir"); + AddDirectory("WorkloadSetsDir", $"workloadsets", "SdkFeatureBandVersionDir"); + AddDirectory("WorkloadSetVersionDir", $"{_package.WorkloadSetVersion}", "WorkloadSetsDir"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SourceDir, $"{packageDataDirectory}"); wixproj.AddPreprocessorDefinition(PreprocessorDefinitionNames.SdkFeatureBandVersion, $"{_package.SdkFeatureBand}"); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs index 0ba3dadc0b4..69a1d18bfc9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -13,14 +13,4 @@ - - - - - - - - - - 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 c48feff2509..9e7c871d427 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Registry.wxs @@ -4,7 +4,7 @@ - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs index 2a03f814627..ab98a8360a2 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/WorkloadSetProduct.wxs @@ -1,5 +1,6 @@ - + @@ -10,9 +11,11 @@ - - + + @@ -25,7 +28,12 @@ - + + + + +