|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Text;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks.Deployment.ManifestUtilities;
using Microsoft.Build.Utilities;
namespace Microsoft.Build.Tasks
{
/// <summary>
/// Generates an application manifest or adds an entry to the existing one when PreferNativeArm64 property is true.
/// </summary>
public sealed class AddToWin32Manifest : TaskExtension
{
private const string supportedArchitectures = "supportedArchitectures";
private const string windowsSettings = "windowsSettings";
private const string application = "application";
private const string asmv3Prefix = "asmv3";
private const string DefaultManifestName = "default.win32manifest";
private const string WindowsSettingsNamespace = "http://schemas.microsoft.com/SMI/2024/WindowsSettings";
private string _outputDirectory = string.Empty;
private string _supportedArchitectures = string.Empty;
private string _generatedManifestFullPath = string.Empty;
/// <summary>
/// Represents the result of validating an application manifest.
/// </summary>
private enum ManifestValidationResult
{
/// <summary>
/// The manifest validation was successful.
/// </summary>
Success = 1,
/// <summary>
/// The manifest validation failed.
/// </summary>
Failure,
/// <summary>
/// The supported architectures exist in the manifest with the expected value.
/// </summary>
SupportedArchitecturesExists,
}
/// <summary>
/// Existing application manifest.
/// </summary>
public ITaskItem? ApplicationManifest { get; set; }
/// <summary>
/// Intermediate output directory.
/// </summary>
[Required]
public string OutputDirectory
{
get => _outputDirectory;
set => _outputDirectory = value ?? throw new ArgumentNullException(nameof(OutputDirectory));
}
/// <summary>
/// Value for supportedArchitectures node.
/// </summary>
[Required]
public string SupportedArchitectures
{
get => _supportedArchitectures;
set => _supportedArchitectures = value ?? throw new ArgumentNullException(nameof(SupportedArchitectures));
}
/// <summary>
/// Returns path to the generated manifest.
/// </summary>
[Output]
public string ManifestPath
{
get => _generatedManifestFullPath;
private set => _generatedManifestFullPath = value;
}
private string? GetManifestPath()
{
if (ApplicationManifest != null)
{
if (string.IsNullOrEmpty(ApplicationManifest.ItemSpec) || !File.Exists(ApplicationManifest?.ItemSpec))
{
Log.LogErrorWithCodeFromResources(null, ApplicationManifest?.ItemSpec, 0, 0, 0, 0, "AddToWin32Manifest.SpecifiedApplicationManifestCanNotBeFound");
return null;
}
return ApplicationManifest!.ItemSpec;
}
string? defaultManifestPath = ToolLocationHelper.GetPathToDotNetFrameworkFile(DefaultManifestName, TargetDotNetFrameworkVersion.Version46);
return defaultManifestPath;
}
private Stream? GetManifestStream(string? path)
{
// The logic for getting default manifest is similar to the one from Roslyn:
// If Roslyn logic returns null, we fall back to reading embedded manifest.
return path is null
? typeof(AddToWin32Manifest).Assembly.GetManifestResourceStream($"Microsoft.Build.Tasks.Resources.{DefaultManifestName}")
: File.OpenRead(path);
}
public override bool Execute()
{
string? manifestPath = GetManifestPath();
try
{
using Stream? stream = GetManifestStream(manifestPath);
if (stream is null)
{
Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpened");
return !Log.HasLoggedErrors;
}
XmlDocument document = LoadManifest(stream);
XmlNamespaceManager xmlNamespaceManager = XmlNamespaces.GetNamespaceManager(document.NameTable);
ManifestValidationResult validationResult = ValidateManifest(manifestPath, document, xmlNamespaceManager);
switch (validationResult)
{
case ManifestValidationResult.Success:
AddSupportedArchitecturesElement(document, xmlNamespaceManager);
SaveManifest(document, Path.GetFileName(ApplicationManifest?.ItemSpec) ?? DefaultManifestName);
return !Log.HasLoggedErrors;
case ManifestValidationResult.SupportedArchitecturesExists:
return !Log.HasLoggedErrors;
case ManifestValidationResult.Failure:
return !Log.HasLoggedErrors;
default:
return false;
}
}
catch (Exception ex)
{
Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpenedWithException", ex.Message);
return !Log.HasLoggedErrors;
}
}
private XmlDocument LoadManifest(Stream stream)
{
XmlDocument document = new XmlDocument();
using (XmlReader xr = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true }))
{
document.Load(xr);
}
return document;
}
private void SaveManifest(XmlDocument document, string manifestName)
{
ManifestPath = Path.Combine(OutputDirectory, manifestName);
using (var xmlWriter = new XmlTextWriter(ManifestPath, Encoding.UTF8))
{
xmlWriter.Formatting = Formatting.Indented;
xmlWriter.Indentation = 4;
document.Save(xmlWriter);
}
}
private ManifestValidationResult ValidateManifest(string? manifestPath, XmlDocument document, XmlNamespaceManager xmlNamespaceManager)
{
if (ApplicationManifest == null)
{
return ManifestValidationResult.Success;
}
XmlNode? assemblyNode = document.SelectSingleNode(XPaths.assemblyElement, xmlNamespaceManager);
if (assemblyNode is null)
{
Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.AssemblyNodeIsMissed");
return ManifestValidationResult.Failure;
}
XmlNode? supportedArchitecturesNode = GetNode(assemblyNode, supportedArchitectures, xmlNamespaceManager);
if (supportedArchitecturesNode != null)
{
if (!string.Equals(supportedArchitecturesNode.InnerText.Trim(), SupportedArchitectures, StringComparison.OrdinalIgnoreCase))
{
Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.InvalidValueInSupportedArchitectures", supportedArchitecturesNode.InnerText);
return ManifestValidationResult.Failure;
}
return ManifestValidationResult.SupportedArchitecturesExists;
}
return ManifestValidationResult.Success;
}
private void AddSupportedArchitecturesElement(XmlDocument document, XmlNamespaceManager xmlNamespaceManager)
{
XmlNode? assemblyNode = document.SelectSingleNode(XPaths.assemblyElement, xmlNamespaceManager);
XmlElement appNode = GetOrCreateXmlElement(document, xmlNamespaceManager, application, asmv3Prefix, XmlNamespaces.asmv3);
XmlElement winSettingsNode = GetOrCreateXmlElement(document, xmlNamespaceManager, windowsSettings, asmv3Prefix, XmlNamespaces.asmv3);
if (string.IsNullOrEmpty(winSettingsNode.GetAttribute(XMakeAttributes.xmlns)))
{
winSettingsNode.SetAttribute(XMakeAttributes.xmlns, WindowsSettingsNamespace);
}
XmlElement supportedArchitecturesNode = GetOrCreateXmlElement(document, xmlNamespaceManager, supportedArchitectures, namespaceURI: WindowsSettingsNamespace);
supportedArchitecturesNode.InnerText = SupportedArchitectures;
winSettingsNode.AppendChild(supportedArchitecturesNode);
// If ParentNode is null, this indicates that winSettingsNode was not a part of the manifest.
if (winSettingsNode.ParentNode == null)
{
appNode.AppendChild(winSettingsNode);
}
if (appNode.ParentNode == null)
{
assemblyNode!.AppendChild(appNode);
}
}
private XmlElement GetOrCreateXmlElement(XmlDocument document, XmlNamespaceManager xmlNamespaceManager, string localName, string prefix = "", string namespaceURI = "")
{
XmlNode? existingNode = GetNode(document, localName, xmlNamespaceManager);
if (existingNode is XmlElement element)
{
return element;
}
return !string.IsNullOrEmpty(prefix)
? document.CreateElement(prefix, localName, namespaceURI)
: document.CreateElement(localName, namespaceURI);
}
private XmlNode? GetNode(XmlNode node, string localName, XmlNamespaceManager xmlNamespaceManager) => node.SelectSingleNode($"//*[local-name()='{localName}']", xmlNamespaceManager);
}
}
|