File: Msi\MsiBase.wix.cs
Web Access
Project: src\src\Microsoft.DotNet.Build.Tasks.Workloads\src\Microsoft.DotNet.Build.Tasks.Workloads.csproj (Microsoft.DotNet.Build.Tasks.Workloads)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.Build.Tasks.Workloads.Wix;
 
namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi
{
    internal abstract class MsiBase
    {
        /// <summary>
        /// Replacement token for license URLs in the generated EULA.
        /// </summary>
        private static readonly string __LICENSE_URL__ = nameof(__LICENSE_URL__);
 
        /// <summary>
        /// Static RTF text for inserting a EULA into the MSI. The license URL of the NuGet package will be embedded 
        /// as plain text since the text control used to render the MSI UI does not render hyperlinks even though RTF supports
        /// it.
        /// </summary>
        internal static readonly string s_eula = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\colortbl ;\red0\green0\blue255;}
{\*\generator Riched20 10.0.19041}\viewkind4\uc1 
\pard\sa200\sl276\slmult1\f0\fs22\lang9 This software is licensed separately as set out in its accompanying license. By continuing, you also agree to that license (__LICENSE_URL__).\par
\par
}";
        /// <summary>
        /// The UUID namespace to use for generating an upgrade code.
        /// </summary>
        internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C");
 
        /// <summary>
        /// Metadata for the MSI such as package ID, version, author information, etc.
        /// </summary>
        public MsiMetadata Metadata
        {
            get;
        }
 
        /// <summary>
        /// The filename of the MSI. The name excludes the platform identifier and extension.
        /// </summary>
        protected abstract string BaseOutputName
        {
            get;
        }
 
        protected IBuildEngine BuildEngine
        {
            get;
        }
 
        /// <summary>
        /// The directory where the compiler output (.wixobj files) will be generated.
        /// </summary>
        protected string CompilerOutputPath
        {
            get;
        }
 
        /// <summary>
        /// The root intermediate output directory. 
        /// </summary>
        protected string BaseIntermediateOutputPath
        {
            get;
        }
 
        /// <summary>
        /// Gets the value to use for the manufacturer. 
        /// </summary>
        protected string Manufacturer =>
            (!string.IsNullOrWhiteSpace(Metadata.Authors) && (Metadata.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) ?
            Metadata.Authors :
            DefaultValues.Manufacturer;
 
        /// <summary>
        /// The platform of the MSI.
        /// </summary>
        protected string Platform
        {
            get;
        }
 
        /// <summary>
        /// The filename of the MSI file to generate.
        /// </summary>
        protected string OutputName => $"{Utils.GetTruncatedHash(BaseOutputName, HashAlgorithmName.SHA256)}-{Platform}.msi";
 
        /// <summary>
        /// The directory where the WiX source code will be generated.
        /// </summary>
        protected string WixSourceDirectory
        {
            get;
        }
 
        /// <summary>
        /// The directory containing the WiX toolset binaries.
        /// </summary>
        protected string WixToolsetPath
        {
            get;
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public Dictionary<string, string> NuGetPackageFiles { get; set; } = new();
 
        public MsiBase(MsiMetadata metadata, IBuildEngine buildEngine, string wixToolsetPath,
            string platform, string baseIntermediateOutputPath)
        {
            BuildEngine = buildEngine;
            WixToolsetPath = wixToolsetPath;
            Platform = platform;
            BaseIntermediateOutputPath = baseIntermediateOutputPath;
 
            // Candle expects the output path to be terminated with a single '\'.
            CompilerOutputPath = Utils.EnsureTrailingSlash(Path.Combine(baseIntermediateOutputPath, "wixobj", metadata.Id, $"{metadata.PackageVersion}", platform));
            WixSourceDirectory = Path.Combine(baseIntermediateOutputPath, "src", "wix", metadata.Id, $"{metadata.PackageVersion}", platform);
            Metadata = metadata;
        }
 
        /// <summary>
        /// Produces an MSI and returns a task item with metadata about the MSI.
        /// </summary>
        /// <param name="outputPath">The directory where the MSI will be generated.</param>
        /// <param name="iceSuppressions">A set of internal consistency evaluators to suppress or <see langword="null"/>.</param>
        /// <returns>An item representing the built MSI.</returns>
        public abstract ITaskItem Build(string outputPath, ITaskItem[]? iceSuppressions);
 
        /// <summary>
        /// Gets the platform specific ProductName MSI property.  
        /// </summary>
        /// <param name="platform">The platform targeted by the MSI.</param>
        /// <returns>A string containing the product name of the MSI.</returns>
        protected string GetProductName(string platform) =>
            (string.IsNullOrWhiteSpace(Metadata.Title) ? Metadata.Id : Metadata.Title) + $" ({platform})";
 
        /// <summary>
        /// Generates a EULA (RTF file) that contains the license URL of the underlying NuGet package.
        /// </summary>
        protected string GenerateEula()
        {
            string eulaRtf = Path.Combine(WixSourceDirectory, "eula.rtf");
            File.WriteAllText(eulaRtf, s_eula.Replace(__LICENSE_URL__, Metadata.LicenseUrl));
 
            return eulaRtf;
        }
 
        /// <summary>
        /// Creates a new compiler tool task and configures some common extensions and preprocessor
        /// variables.
        /// </summary>
        /// <returns></returns>
        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;
        }
 
        /// <summary>
        /// Links the MSI using the output from the WiX compiler using a default set of WiX extensions.
        /// </summary>
        /// <param name="compilerOutputPath">The path where the output of the compiler (.wixobj files) will be generated.</param>
        /// <param name="outputFile">The full path of the MSI to create during linking.</param>
        /// <param name="iceSuppressions">A set of internal consistency evaluators to suppress. May be <see langword="null"/>.</param>
        /// <returns>An <see cref="ITaskItem"/> for the MSI that was created.</returns>
        /// <exception cref="Exception"></exception>
        protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions = null)
        {
            return Link(compilerOutputPath, outputFile, iceSuppressions, WixExtensions.WixDependencyExtension,
                WixExtensions.WixUIExtension, WixExtensions.WixUtilExtension);
        }
 
        /// <summary>
        /// Links the MSI using the output from the WiX compiler and a set of WiX extensions.
        /// </summary>
        /// <param name="compilerOutputPath">The path where the output of the compiler (.wixobj files) can be found.</param>
        /// <param name="outputFile">The full path of the MSI to create during linking.</param>
        /// <param name="iceSuppressions">A set of internal consistency evaluators to suppress. May be <see langword="null"/>.</param>
        /// <param name="wixExtensions">A list of WiX extensions to include when linking the MSI.</param>
        /// <returns>An <see cref="ITaskItem"/> for the MSI that was created.</returns>
        /// <exception cref="Exception"></exception>
        protected ITaskItem Link(string compilerOutputPath, string outputFile, ITaskItem[]? iceSuppressions, params string[] wixExtensions)
        {
            // Link the MSI. The generated filename contains the semantic version (excluding build metadata) and platform. 
            // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add
            // the platform again.
            LinkerToolTask light = new(BuildEngine, WixToolsetPath)
            {
                OutputFile = outputFile,
                SourceFiles = Directory.EnumerateFiles(compilerOutputPath, "*.wixobj"),
                SuppressIces = iceSuppressions == null ? null : string.Join(";", iceSuppressions.Select(i => i.ItemSpec))
            };
 
            foreach (string wixExtension in wixExtensions)
            {
                light.AddExtension(wixExtension);
            }
 
            if (!light.Execute())
            {
                throw new Exception(Strings.FailedToLinkMsi);
            }
 
            TaskItem msiItem = new TaskItem(light.OutputFile);
 
            // Return a task item that contains all the information about the generated MSI.            
            msiItem.SetMetadata(Workloads.Metadata.Platform, Platform);
            msiItem.SetMetadata(Workloads.Metadata.WixObj, compilerOutputPath);
            msiItem.SetMetadata(Workloads.Metadata.Version, $"{Metadata.MsiVersion}");
            msiItem.SetMetadata(Workloads.Metadata.SwixPackageId, Metadata.SwixPackageId);
 
            return msiItem;
        }
 
        protected void AddDefaultPackageFiles(ITaskItem msi)
        {
            NuGetPackageFiles[msi.GetMetadata(Workloads.Metadata.FullPath)] = @"\data";
 
            // Create the JSON manifest for CLI based installations.
            string msiJsonPath = MsiProperties.Create(msi.ItemSpec);
            NuGetPackageFiles[Path.GetFullPath(msiJsonPath)] = "\\data\\msi.json";
 
            NuGetPackageFiles["LICENSE.TXT"] = @"\";
        }
    }
}
 
#nullable disable