File: src\CreateWixCommandPackageDropBase.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.Installers.csproj (Microsoft.DotNet.Build.Tasks.Installers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Microsoft.DotNet.Build.Tasks.Installers;

public abstract class CreateWixCommandPackageDropBase : Task, IMultiThreadableTask
{
    /// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

    private const int _fieldsArtifactId = 0;
    private const int _fieldsArtifactPath1 = 6;
    private const int _fieldsArtifactPath2 = 1;
    private const int _fieldsArtifactPath3 = 2;
    private const int _fieldsArtifactPath6 = 5;

    private readonly string _packageExtension = ".wixpack.zip";
    public bool NoLogo { get; set; }
    /// <summary>
    /// Additional set of base paths that are used for resolving paths.
    /// </summary>
    public ITaskItem[] AdditionalBasePaths { get; set; }
    /// <summary>
    /// Localization files
    /// </summary>
    public ITaskItem[] Loc { get; set; }
    [Required]
    public string InstallerFile { get; set; }
    public ITaskItem[] WixExtensions { get; set; }

    /// <summary>
    /// folder to place wixpackage output file
    /// </summary>
    [Required]
    public string OutputFolder { get; set; }
    [Required]
    public ITaskItem[] WixSrcFiles { get; set; }

    /// <summary>
    /// path of wixpackage file
    /// </summary>
    [Output]
    public string OutputFile { get; set; }

    protected abstract void ProcessToolSpecificCommandLineParameters(AbsolutePath packageDropDir, StringBuilder commandString);

    protected void ProcessWixCommand(string packageDropOutputFolder, string toolExecutable, string originalCommand)
    {
        AbsolutePath packageDropDir = TaskEnvironment.GetAbsolutePath(packageDropOutputFolder);
        if (!Directory.Exists(packageDropDir))
        {
            Directory.CreateDirectory(packageDropDir);
        }

        ProcessWixSrcFiles(packageDropDir);

        ProcessLocFiles(packageDropDir);

        CreateCommandFile(toolExecutable, originalCommand, packageDropDir);

        // OutputFile is an [Output] property, so it keeps the form the project supplied through
        // OutputFolder. Only the copy used for file I/O below is resolved.
        OutputFile = Path.Combine(OutputFolder, $"{Path.GetFileName(InstallerFile)}{_packageExtension}");
        AbsolutePath outputFilePath = TaskEnvironment.GetAbsolutePath(OutputFile);
        if(File.Exists(outputFilePath))
        {
            File.Delete(outputFilePath);
        }
        AbsolutePath outputFolderPath = TaskEnvironment.GetAbsolutePath(OutputFolder);
        if(!Directory.Exists(outputFolderPath))
        {
            Directory.CreateDirectory(outputFolderPath);
        }
        ZipFile.CreateFromDirectory(packageDropDir, outputFilePath);
    }

    private void CreateCommandFile(string toolExecutable, string originalCommand, AbsolutePath packageDropDir)
    {
        AbsolutePath commandFilename = TaskEnvironment.GetAbsolutePath(Path.Combine(packageDropDir, $"create.cmd"));
        StringBuilder commandString = new StringBuilder();
        commandString.AppendLine("@echo off");
        commandString.AppendLine("set outputfolder=%1");
        commandString.AppendLine("if \"%outputfolder%\" NEQ \"\" (");
        commandString.AppendLine("  if \"%outputfolder:~-1%\" NEQ \"\\\" ( ");
        commandString.AppendLine("    set outputfolder=%outputfolder%\\");
        commandString.AppendLine("  )");
        commandString.AppendLine(")");
        if (originalCommand != null)
        {
            commandString.AppendLine($"REM Original command");
            commandString.AppendLine($"REM {originalCommand }");
        }
        commandString.AppendLine("REM Modified command");
        commandString.Append(toolExecutable);
        commandString.Append($" -out %outputfolder%{Path.GetFileName(InstallerFile)}");
        if (NoLogo)
        {
            commandString.Append(" -nologo");
        }
        if (Loc != null)
        {
            foreach (var locItem in Loc)
            {
                commandString.Append($" -loc {Path.GetFileName(locItem.ItemSpec)}");
            }
        }
        if (WixExtensions != null)
        {
            foreach (var wixExtension in WixExtensions)
            {
                commandString.Append($" -ext {wixExtension.ItemSpec}");
            }
        }
        if (WixSrcFiles != null)
        {
            foreach (var wixSrcFile in WixSrcFiles)
            {
                commandString.Append($" {Path.GetFileName(wixSrcFile.ItemSpec)}");
            }
        }
        ProcessToolSpecificCommandLineParameters(packageDropDir, commandString);
        commandString.AppendLine();
        if(!Directory.Exists(packageDropDir))
        {
            Directory.CreateDirectory(packageDropDir);
        }
        File.WriteAllText(commandFilename, commandString.ToString());
    }

    /// <summary>
    ///     Process each of the wix src files
    /// </summary>
    /// <param name="packageDropDir">Drop folder to place artifacts</param>
    private void ProcessWixSrcFiles(AbsolutePath packageDropDir)
    {
        XmlNamespaceManager nsmgr = new XmlNamespaceManager(new NameTable());
        nsmgr.AddNamespace("wix", "http://schemas.microsoft.com/wix/2006/objects");

        foreach (var wixSrcFile in WixSrcFiles)
        {
            // copy the file to outputPath
            AbsolutePath newWixSrcFilePath = TaskEnvironment.GetAbsolutePath(Path.Combine(packageDropDir, Path.GetFileName(wixSrcFile.ItemSpec)));
            File.Copy(TaskEnvironment.GetAbsolutePath(wixSrcFile.ItemSpec), newWixSrcFilePath, true);

            string wixSrcFileExtension = Path.GetExtension(wixSrcFile.ItemSpec);
            // These files are typically .wixobj. Occasionally we have a wixlib as input, which
            // is created using light and is a binary file. When doing post-build signing,
            // it's replaced in the inputs to the light command after being reconstructed from
            // its own light command drop.
            if (wixSrcFileExtension == ".wixlib")
            {
                continue;
            }
            else if (wixSrcFileExtension != ".wixobj")
            {
                Log.LogError($"Wix source file extension {wixSrcFileExtension} is not supported.");
                continue;
            }

            ProcessWixObj(newWixSrcFilePath, packageDropDir, nsmgr);
        }
    }

    /// <summary>
    ///     Process the .wxl files and copy to the local drop folder
    /// </summary>
    /// <param name="packageDropDir">Drop location for wxl files</param>
    private void ProcessLocFiles(AbsolutePath packageDropDir)
    {
        if (Loc != null)
        {
            foreach (var locItem in Loc)
            {
                AbsolutePath destinationPath = TaskEnvironment.GetAbsolutePath(Path.Combine(packageDropDir, Path.GetFileName(locItem.ItemSpec)));
                File.Copy(TaskEnvironment.GetAbsolutePath(locItem.ItemSpec), destinationPath, true);
            }
        }
    }

    /// <summary>
    ///     Process a .wixobj file that is an input to the light/lit command.
    /// </summary>
    /// <param name="wixObjFilePath">Path to the wixobj file in its new drop location</param>
    /// <param name="packageDropDir">Output light/lit command drop folder</param>
    /// <param name="nsmgr">xml namespace manager</param>
    private void ProcessWixObj(AbsolutePath wixObjFilePath, AbsolutePath packageDropDir, XmlNamespaceManager nsmgr)
    {
        Log.LogMessage(MessageImportance.Normal, $"Creating modified wixobj file '{wixObjFilePath}'...");

        XDocument doc = XDocument.Load(wixObjFilePath);
        if (doc == null)
        {
            Log.LogError($"Failed to open the wixobj file '{wixObjFilePath}'");
            return;
        }

        // process fragment - WixFile elements
        // path in field 7
        string xpath = "//wix:wixObject/wix:section[@type='fragment']/wix:table[@name='WixFile']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath1);

        // process product - WixFile elements
        // path in field 7
        xpath = "//wix:wixObject/wix:section[@type='product']/wix:table[@name='WixFile']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath1);

        // process fragment - Binary elements
        // path in field 2
        xpath = "//wix:wixObject/wix:section[@type='fragment']/wix:table[@name='Binary']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath2);

        // process product - Icon elements
        // path in field 2
        xpath = "//wix:wixObject/wix:section[@type='product']/wix:table[@name='Icon']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath2);

        // process product - WixVariable elements
        // path in field 2
        xpath = "//wix:wixObject/wix:section[@type='product']/wix:table[@name='WixVariable']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath2);

        // Bundle specific items.

        // path in fields 3 and 6
        xpath = "//wix:wixObject/wix:section[@type='bundle']/wix:table[@name='Payload']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath3, _fieldsArtifactPath6);

        // process WixVariable data
        // path in field 2
        xpath = "//wix:wixObject/wix:section[@type='bundle']/wix:table[@name='WixVariable']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath2);

        // process Payload, in fragment section, data
        // path in fields 3 and 6
        xpath = "//wix:wixObject/wix:section[@type='fragment']/wix:table[@name='Payload']/wix:row";
        ProcessXPath(doc, xpath, packageDropDir, nsmgr, _fieldsArtifactPath3, _fieldsArtifactPath6);

        doc.Save(wixObjFilePath);
    }

    private void ProcessXPath(XDocument doc, string xpath, AbsolutePath outputPath, XmlNamespaceManager nsmgr, int pathField1, int pathField2 = 0)
    {
        IEnumerable<XElement> iels = doc.XPathSelectElements(xpath, nsmgr);
        if (iels != null && iels.Count() > 0)
        {

            foreach (XElement row in iels)
            {
                IEnumerable<XElement> fields = row.XPathSelectElements("wix:field", nsmgr);
                if (fields == null || fields.Count() == 0)
                {
                    Log.LogError($"No fields in row ('{xpath}') of document '{doc.BaseUri}'");
                    continue;
                }

                int count = 0;
                string id = null;
                string oldPath = null;
                string newRelativePath = null;
                bool foundArtifact = false;
                bool isVariableOrUriRef = false;

                foreach (XElement field in fields)
                {
                    if (count == _fieldsArtifactId)
                    {
                        id = field.Value;
                    }
                    else if (count == pathField1)
                    {
                        oldPath = field.Value;

                        // Potentially make oldPath the absolute if it's not, using the additional base
                        // paths. It's possible that the path is a variable or URI. In this case,
                        // we can ignore it.
                        if (oldPath.StartsWith("!(") || oldPath.StartsWith("https"))
                        {
                            isVariableOrUriRef = true;
                            break;
                        }
                        else if (!Path.IsPathRooted(oldPath))
                        {
                            if (AdditionalBasePaths == null)
                            {
                                // Break here, will log an error below.
                                break;
                            }
                            foreach (var additionalBasePath in AdditionalBasePaths)
                            {
                                AbsolutePath possiblePath = TaskEnvironment.GetAbsolutePath(Path.Combine(additionalBasePath.ItemSpec, oldPath));
                                if (File.Exists(possiblePath))
                                {
                                    oldPath = possiblePath;
                                    foundArtifact = true;
                                    break;
                                }
                            }
                        }
                        else if (File.Exists(TaskEnvironment.GetAbsolutePath(oldPath)))
                        {
                            foundArtifact = true;
                        }
                        else
                        {
                            break;
                        }

                        newRelativePath = Path.Combine(id, Path.GetFileName(oldPath));
                        field.Value = newRelativePath;
                    }
                    else if (pathField2 != 0 && count == pathField2)
                    {
                        field.Value = newRelativePath;
                        break;
                    }
                    count++;
                }

                if (!isVariableOrUriRef)
                {
                    if (foundArtifact)
                    {
                        AbsolutePath newFolder = TaskEnvironment.GetAbsolutePath(Path.Combine(outputPath, id));
                        if (!Directory.Exists(newFolder))
                        {
                            Directory.CreateDirectory(newFolder);
                        }

                        File.Copy(TaskEnvironment.GetAbsolutePath(oldPath), TaskEnvironment.GetAbsolutePath(Path.Combine(outputPath, newRelativePath)), true);
                    }
                    else if (oldPath == null)
                    {
                        Log.LogError($"Could not locate a file within {row}");
                    }
                    else
                    {
                        Log.LogError($"Could not locate file {oldPath}. Please ensure the file exists and/or pass AdditionalBasePaths for non-rooted file paths.");
                    }
                }
            }
        }
    }
}