diff --git a/eng/Versions.props b/eng/Versions.props index 0d7fe6de34d..363d0b88387 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,5 +84,6 @@ 1.1.156602 1.1.156602 6.0.100-preview.5.21254.11 + 1.0.0-preview1.1.21116.1 diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs index ffb6b3739e9..1ad0d3a7662 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/EmbeddedTemplates.cs @@ -64,6 +64,7 @@ static EmbeddedTemplates() { { "DependencyProvider.wxs", $"{s_namespace}.MsiTemplate.DependencyProvider.wxs" }, { "Directories.wxs", $"{s_namespace}.MsiTemplate.Directories.wxs" }, + { "ManifestProduct.wxs", $"{s_namespace}.MsiTemplate.ManifestProduct.wxs" }, { "Product.wxs", $"{s_namespace}.MsiTemplate.Product.wxs" }, { "Registry.wxs", $"{s_namespace}.MsiTemplate.Registry.wxs" }, { "Variables.wxi", $"{s_namespace}.MsiTemplate.Variables.wxi" }, diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.cs new file mode 100644 index 00000000000..899f010c8b2 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/GenerateManifestMsi.cs @@ -0,0 +1,348 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + public class GenerateManifestMsi : GenerateTaskBase + { + private Version _sdkFeaureBandVersion; + + /// + /// The path where the generated MSIs will be placed. + /// + [Required] + public string OutputPath + { + get; + set; + } + + /// + /// The ID of the workload manifest. + /// + [Required] + public string ManifestId + { + get; + set; + } + + /// + /// The set of MSIs that were produced. + /// + [Output] + public ITaskItem[] Msis + { + get; + protected set; + } + + private Version SdkFeatureBandVersion + { + get + { + if (_sdkFeaureBandVersion == null) + { + ReleaseVersion sdkReleaseVersion = new ReleaseVersion(SdkVersion); + + _sdkFeaureBandVersion = new($"{sdkReleaseVersion.Major}.{sdkReleaseVersion.Minor}.{sdkReleaseVersion.SdkFeatureBand}"); + } + + return _sdkFeaureBandVersion; + } + } + + /// + /// The SDK version, e.g. 6.0.107. + /// + [Required] + public string SdkVersion + { + get; + set; + } + + /// + /// Semicolon sepearate list of ICEs to suppress. + /// + public string SuppressIces + { + get; + set; + } + + /// + /// The version of the MSI. + /// + [Required] + public string MsiVersion + { + get; + set; + } + + [Required] + public string WorkloadManifestPackage + { + get; + set; + } + + public override bool Execute() + { + try + { + Log.LogMessage($"Generating workload manifest installer for {SdkFeatureBandVersion}"); + + NugetPackage nupkg = new(WorkloadManifestPackage, Log); + List msis = new(); + + // MSI ProductName defaults to the package title and fallback to the package ID with a warning. + string productName = nupkg.Title; + + if (string.IsNullOrWhiteSpace(nupkg.Title)) + { + Log?.LogMessage(MessageImportance.High, $"'{WorkloadManifestPackage}' should have a non-empty title. The MSI ProductName will be set to the package ID instead."); + productName = nupkg.Id; + } + + // Extract once, but harvest multiple times because some generated attributes are platform dependent. + string packageContentsDirectory = Path.Combine(PackageDirectory, $"{nupkg.Identity}"); + nupkg.Extract(packageContentsDirectory, Enumerable.Empty()); + string packageContentsDataDirectory = Path.Combine(packageContentsDirectory, "data"); + + foreach (string platform in GenerateMsiBase.SupportedPlatforms) + { + // Extract the MSI template and add it to the list of source files. + List sourceFiles = new(); + string msiSourcePath = Path.Combine(MsiDirectory, $"{nupkg.Id}", $"{nupkg.Version}", platform); + sourceFiles.Add(EmbeddedTemplates.Extract("DependencyProvider.wxs", msiSourcePath)); + sourceFiles.Add(EmbeddedTemplates.Extract("ManifestProduct.wxs", msiSourcePath)); + + string EulaRtfPath = Path.Combine(msiSourcePath, "eula.rtf"); + File.WriteAllText(EulaRtfPath, GenerateMsiBase.Eula.Replace("__LICENSE_URL__", nupkg.LicenseUrl)); + EmbeddedTemplates.Extract("Variables.wxi", msiSourcePath); + + // Harvest the package contents and add it to the source files we need to compile. + string packageContentWxs = Path.Combine(msiSourcePath, "PackageContent.wxs"); + sourceFiles.Add(packageContentWxs); + + HarvestToolTask heat = new(BuildEngine, WixToolsetPath) + { + ComponentGroupName = GenerateMsiBase.PackageContentComponentGroupName, + DirectoryReference = "ManifestIdDir", + OutputFile = packageContentWxs, + Platform = platform, + SourceDirectory = packageContentsDataDirectory + }; + + if (!heat.Execute()) + { + throw new Exception($"Failed to harvest package contents."); + } + + // To support upgrades, the UpgradeCode must be stable withing a feature band. + // For example, 6.0.101 and 6.0.108 will generate the same GUID for the same platform. + var upgradeCode = Utils.CreateUuid(GenerateMsiBase.UpgradeCodeNamespaceUuid, $"{SdkFeatureBandVersion};{platform}"); + var productCode = Guid.NewGuid(); + Log.LogMessage($"UC: {upgradeCode}, PC: {productCode}, {SdkFeatureBandVersion}, {SdkVersion}, {platform}"); + + // Compile the MSI sources + string candleIntermediateOutputPath = Path.Combine(IntermediateBaseOutputPath, "wixobj", + $"{nupkg.Id}", $"{nupkg.Version}", platform); + + CompileToolTask candle = new(BuildEngine, WixToolsetPath) + { + // Candle expects the output path to end with a single '\' + OutputPath = candleIntermediateOutputPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, + SourceFiles = sourceFiles, + Arch = platform + }; + + // Configure preprocessor definitions. + string manufacturer = "Microsoft Corporation"; + + if (!string.IsNullOrWhiteSpace(nupkg.Authors) && (nupkg.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) + { + manufacturer = nupkg.Authors; + } + Log.LogMessage(MessageImportance.Low, $"Setting Manufacturer to '{manufacturer}'"); + + candle.PreprocessorDefinitions.Add($@"PackageId={nupkg.Id}"); + candle.PreprocessorDefinitions.Add($@"PackageVersion={nupkg.Version}"); + candle.PreprocessorDefinitions.Add($@"ProductVersion={MsiVersion}"); + candle.PreprocessorDefinitions.Add($@"ProductCode={productCode}"); + candle.PreprocessorDefinitions.Add($@"UpgradeCode={upgradeCode}"); + // Override the default provider key + candle.PreprocessorDefinitions.Add($@"DependencyProviderKey={nupkg.Id},{platform}"); + candle.PreprocessorDefinitions.Add($@"ProductName={productName}"); + candle.PreprocessorDefinitions.Add($@"Platform={platform}"); + candle.PreprocessorDefinitions.Add($@"SourceDir={packageContentsDataDirectory}"); + candle.PreprocessorDefinitions.Add($@"Manufacturer={manufacturer}"); + candle.PreprocessorDefinitions.Add($@"EulaRtf={EulaRtfPath}"); + candle.PreprocessorDefinitions.Add($@"SdkFeatureBandVersion={SdkFeatureBandVersion}"); + + // The temporary installer in the SDK used lower invariants of the manifest ID. + // We have to do the same to ensure the keypath generation produces stable GUIDs so that + // the manifests/targets get the same component GUIDs. + candle.PreprocessorDefinitions.Add($@"ManifestId={ManifestId.ToLowerInvariant()}"); + + // Compiler extension to process dependency provider authoring for package reference counting. + candle.Extensions.Add("WixDependencyExtension"); + + if (!candle.Execute()) + { + throw new Exception($"Failed to compile MSI."); + } + + // Link the MSI. The generated filename contains a the semantic version (excluding build metadata) and platform. + // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add + // the platform again. + + string shortPackageName = Path.GetFileNameWithoutExtension(WorkloadManifestPackage); + + string outputFile = Path.Combine(OutputPath, shortPackageName + $"-{platform}.msi"); + + LinkToolTask light = new(BuildEngine, WixToolsetPath) + { + OutputFile = Path.Combine(OutputPath, outputFile), + SourceFiles = Directory.EnumerateFiles(candleIntermediateOutputPath, "*.wixobj"), + SuppressIces = this.SuppressIces + }; + + // Add WiX extensions + light.Extensions.Add("WixDependencyExtension"); + light.Extensions.Add("WixUIExtension"); + + if (!light.Execute()) + { + throw new Exception($"Failed to link MSI."); + } + + // Generate metadata used for CLI based installations. + string msiPath = light.OutputFile; + MsiProperties msiProps = new MsiProperties + { + InstallSize = MsiUtils.GetInstallSize(msiPath), + Payload = Path.GetFileName(msiPath), + ProductCode = MsiUtils.GetProperty(msiPath, "ProductCode"), + ProductVersion = MsiUtils.GetProperty(msiPath, "ProductVersion"), + ProviderKeyName = $"{nupkg.Id},{nupkg.Version},{platform}", + UpgradeCode = MsiUtils.GetProperty(msiPath, "UpgradeCode") + }; + + string msiJsonPath = Path.Combine(Path.GetDirectoryName(msiPath), Path.GetFileNameWithoutExtension(msiPath) + ".json"); + File.WriteAllText(msiJsonPath, JsonSerializer.Serialize(msiProps)); + + TaskItem msi = new(light.OutputFile); + msi.SetMetadata(Metadata.Platform, platform); + msi.SetMetadata(Metadata.Version, nupkg.ProductVersion); + msi.SetMetadata(Metadata.JsonProperties, msiJsonPath); + msi.SetMetadata(Metadata.WixObj, candleIntermediateOutputPath); + + // Generate a .csproj to build a NuGet payload package to carry the MSI and JSON manifest + msi.SetMetadata(Metadata.PackageProject, GeneratePackageProject(msi.ItemSpec, msiJsonPath, platform, nupkg)); + + msis.Add(msi); + } + + Msis = msis.ToArray(); + } + catch (Exception e) + { + Log.LogErrorFromException(e); + } + + return !Log.HasLoggedErrors; + } + + + private string GeneratePackageProject(string msiPath, string msiJsonPath, string platform, NugetPackage nupkg) + { + string msiPackageProject = Path.Combine(MsiPackageDirectory, platform, nupkg.Id, "msi.csproj"); + string msiPackageProjectDir = Path.GetDirectoryName(msiPackageProject); + + Log?.LogMessage($"Generating package project: '{msiPackageProject}'"); + + if (Directory.Exists(msiPackageProjectDir)) + { + Directory.Delete(msiPackageProjectDir, recursive: true); + } + + Directory.CreateDirectory(msiPackageProjectDir); + + EmbeddedTemplates.Extract("Icon.png", msiPackageProjectDir); + EmbeddedTemplates.Extract("LICENSE.TXT", msiPackageProjectDir); + + string licenseTextPath = Path.Combine(msiPackageProjectDir, "LICENSE.TXT"); + + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + IndentChars = " ", + }; + + XmlWriter writer = XmlWriter.Create(msiPackageProject, settings); + + writer.WriteStartElement("Project"); + writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); + + writer.WriteStartElement("PropertyGroup"); + writer.WriteElementString("TargetFramework", "net5.0"); + writer.WriteElementString("GeneratePackageOnBuild", "true"); + writer.WriteElementString("IncludeBuildOutput", "false"); + writer.WriteElementString("IsPackable", "true"); + writer.WriteElementString("PackageType", "DotnetPlatform"); + writer.WriteElementString("SuppressDependenciesWhenPacking", "true"); + writer.WriteElementString("NoWarn", "$(NoWarn);NU5128"); + writer.WriteElementString("PackageId", $"{nupkg.Id}.Msi.{platform}"); + writer.WriteElementString("PackageVersion", $"{nupkg.Version}"); + writer.WriteElementString("Description", nupkg.Description); + writer.WriteElementString("PackageIcon", "Icon.png"); + + if (!string.IsNullOrWhiteSpace(nupkg.Authors)) + { + writer.WriteElementString("Authors", nupkg.Authors); + } + + if (!string.IsNullOrWhiteSpace(nupkg.Copyright)) + { + writer.WriteElementString("Copyright", nupkg.Copyright); + } + + writer.WriteElementString("PackageLicenseExpression", "MIT"); + writer.WriteEndElement(); + + writer.WriteStartElement("ItemGroup"); + WriteItem(writer, "None", msiPath, @"\data"); + WriteItem(writer, "None", msiJsonPath, @"\data\msi.json"); + WriteItem(writer, "None", licenseTextPath, @"\"); + writer.WriteEndElement(); + + writer.WriteEndElement(); + writer.Flush(); + writer.Close(); + + return msiPackageProject; + } + + private void WriteItem(XmlWriter writer, string itemName, string include, string packagePath) + { + writer.WriteStartElement(itemName); + writer.WriteAttributeString("Include", include); + writer.WriteAttributeString("Pack", "true"); + writer.WriteAttributeString("PackagePath", packagePath); + writer.WriteEndElement(); + } + } +} 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 ae34705aed9..161b0654020 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 @@ -22,6 +22,7 @@ + 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 e3bc884ceee..66cbae46436 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Directories.wxs @@ -6,11 +6,9 @@ - - - - - + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs new file mode 100644 index 00000000000..b5556c28d19 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/ManifestProduct.wxs @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + NOT WIX_DOWNGRADE_DETECTED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi index 72c0e6d4c9b..0c5e3b07d49 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/MsiTemplate/Variables.wxi @@ -17,5 +17,8 @@ + + +