File: src\CreateFrameworkListFile.cs
Web Access
Project: src\src\Microsoft.DotNet.SharedFramework.Sdk\Microsoft.DotNet.SharedFramework.Sdk.csproj (Microsoft.DotNet.SharedFramework.Sdk)
// 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.DotNet.Build.Tasks;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
 
namespace Microsoft.DotNet.SharedFramework.Sdk
{
    public class CreateFrameworkListFile : BuildTask
    {
        /// <summary>
        /// Files to extract basic information from and include in the list.
        /// </summary>
        [Required]
        public ITaskItem[] Files { get; set; }
 
        /// <summary>
        /// A list of assembly names with classification info such as Profile. A
        /// Profile="%(Profile)" attribute is included in the framework list for the matching Files
        /// item if %(Profile) contains text.
        ///
        /// If *any* FileClassifications are passed:
        ///
        ///   *Every* file that ends up listed in the framework list must have a matching
        ///   FileClassification.
        ///
        ///   Additionally, every FileClassification must find exactly one File.
        ///
        /// This task fails if the conditions aren't met. This ensures the classification doesn't
        /// become out of date when the list of files changes.
        ///
        /// %(Identity): Assembly name (including ".dll").
        /// %(Profile): List of profiles that apply, semicolon-delimited.
        /// %(ReferencedByDefault): Empty (default) or "true"/"false". Indicates whether this file
        ///   should be referenced by default when the SDK uses this framework.
        /// </summary>
        public ITaskItem[] FileClassifications { get; set; }
 
        [Required]
        public string TargetFile { get; set; }
 
        public string[] TargetFilePrefixes { get; set; }
 
        public string[] SingleFileHostIncludeFilenames { get; set; }
 
        /// <summary>
        /// Extra attributes to place on the root node.
        /// 
        /// %(Identity): Attribute name.
        /// %(Value): Attribute value.
        /// </summary>
        public ITaskItem[] RootAttributes { get; set; }
 
        public override bool Execute()
        {
            XAttribute[] rootAttributes = RootAttributes
                ?.Select(item => new XAttribute(item.ItemSpec, item.GetMetadata("Value")))
                .ToArray();
 
            var frameworkManifest = new XElement("FileList", rootAttributes);
 
            Dictionary<string, ITaskItem> fileClassLookup = FileClassifications
                ?.ToDictionary(
                    item => item.ItemSpec,
                    item => item,
                    StringComparer.OrdinalIgnoreCase);
 
            var singleFileHostIncludeFilenames = SingleFileHostIncludeFilenames?.ToHashSet();
 
            var usedFileClasses = new HashSet<string>();
 
            foreach (var f in Files
                .Where(IsTargetPathIncluded)
                .Select(item => new
                {
                    Item = item,
                    Filename = Path.GetFileName(item.ItemSpec),
                    TargetPath = item.GetMetadata("TargetPath"),
                    AssemblyName = FileUtilities.GetAssemblyName(item.ItemSpec),
                    FileVersion = FileUtilities.GetFileVersion(item.ItemSpec),
                    IsNative = item.GetMetadata("IsNative") == "true",
                    IsSymbolFile = item.GetMetadata("IsSymbolFile") == "true",
                    IsPgoData = item.GetMetadata("IsPgoData") == "true",
                    IsResourceFile = item.ItemSpec
                        .EndsWith(".resources.dll", StringComparison.OrdinalIgnoreCase)
                })
                .Where(f =>
                    !f.IsSymbolFile &&
                    (f.Filename.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || f.IsNative || f.IsPgoData))
                // Remove duplicate files this task is given.
                .GroupBy(f => f.Item.ItemSpec)
                .Select(g => g.First())
                // Make order stable between builds.
                .OrderBy(f => f.TargetPath, StringComparer.Ordinal)
                .ThenBy(f => f.Filename, StringComparer.Ordinal))
            {
                string type = "Managed";
 
                if (f.IsNative)
                {
                    type = "Native";
                }
                else if (f.IsResourceFile)
                {
                    type = "Resources";
                }
                else if (f.IsPgoData)
                {
                    type = "PgoData";
                }
 
                string path = Path.Combine(f.TargetPath, f.Filename).Replace('\\', '/');
 
                if (path.StartsWith("runtimes/"))
                {
                    var pathParts = path.Split('/');
                    if (pathParts.Length > 1 && pathParts[1].Contains("_"))
                    {
                        // This file is a runtime file with a "rid" containing "_". This is assumed
                        // to mean it's a cross-targeting tool and shouldn't be deployed in a
                        // self-contained app. Leave it off the list.
                        continue;
                    }
                }
 
                string analyzerLanguage = null;
 
                if (path.StartsWith("analyzers/"))
                {
                    type = "Analyzer";
 
                    if (path.EndsWith(".resources.dll"))
                    {
                        // omit analyzer resources
                        continue;
                    }
 
                    var pathParts = path.Split('/');
 
                    if (pathParts.Length < 3 || !pathParts[1].Equals("dotnet", StringComparison.Ordinal) || pathParts.Length > 4)
                    {
                        Log.LogError($"Unexpected analyzer path format {path}.  Expected  'analyzers/dotnet(/language)/analyzer.dll");
                    }
 
                    if (pathParts.Length > 3)
                    {
                        analyzerLanguage = pathParts[2];
                    }
                }
 
                var element = new XElement(
                    "File",
                    new XAttribute("Type", type),
                    new XAttribute("Path", path));
 
                if (analyzerLanguage != null)
                {
                    element.Add(new XAttribute("Language", analyzerLanguage));
                }
 
                if (f.IsResourceFile)
                {
                    element.Add(
                        new XAttribute("Culture", Path.GetFileName(Path.GetDirectoryName(path))));
                }
 
                if (f.IsPgoData)
                {
                    // Pgo data is never carried with single file images
                    element.Add(new XAttribute("DropFromSingleFile", "true"));
                }
                else if (f.AssemblyName != null)
                {
                    byte[] publicKeyToken = f.AssemblyName.GetPublicKeyToken();
                    string publicKeyTokenHex;
 
                    if (publicKeyToken != null)
                    {
                        int len = publicKeyToken.Length;
                        StringBuilder publicKeyTokenBuilder = new StringBuilder(len * 2);
                        for (int i = 0; i < len; i++)
                        {
                            publicKeyTokenBuilder.Append(publicKeyToken[i].ToString("x2", CultureInfo.InvariantCulture));
                        }
                        publicKeyTokenHex = publicKeyTokenBuilder.ToString();
                    }
                    else
                    {
                        Log.LogError($"No public key token found for assembly {f.Item.ItemSpec}");
                        publicKeyTokenHex = "";
                    }
 
                    element.Add(
                        new XAttribute("AssemblyName", f.AssemblyName.Name),
                        new XAttribute("PublicKeyToken", publicKeyTokenHex),
                        new XAttribute("AssemblyVersion", f.AssemblyName.Version));
                }
                else if (!f.IsNative)
                {
                    // This file isn't managed and isn't native. Leave it off the list.
                    continue;
                }
 
                element.Add(new XAttribute("FileVersion", f.FileVersion));
 
                if (fileClassLookup != null)
                {
                    if (fileClassLookup.TryGetValue(f.Filename, out ITaskItem classItem))
                    {
                        string profile = classItem.GetMetadata("Profile");
 
                        if (!string.IsNullOrEmpty(profile))
                        {
                            element.Add(new XAttribute("Profile", profile));
                        }
 
                        string referencedByDefault = classItem.GetMetadata("ReferencedByDefault");
 
                        if (!string.IsNullOrEmpty(referencedByDefault))
                        {
                            element.Add(new XAttribute("ReferencedByDefault", referencedByDefault));
                        }
 
                        usedFileClasses.Add(f.Filename);
                    }
                    else
                    {
                        Log.LogError($"File matches no classification: {f.Filename}");
                    }
                }
 
                if (f.IsNative)
                {
                    // presence of inclusion list indicates that 
                    // all other native files should be marked as "DropFromSingleFile"
                    if (singleFileHostIncludeFilenames != null &&
                        !singleFileHostIncludeFilenames.Contains(f.Filename))
                    {
                        element.Add(new XAttribute("DropFromSingleFile", "true"));
                    }
                }
 
                frameworkManifest.Add(element);
            }
 
            foreach (var unused in fileClassLookup
                ?.Keys.Except(usedFileClasses).OrderBy(p => p)
                ?? Enumerable.Empty<string>())
            {
                Log.LogError($"Classification matches no files: {unused}");
            }
 
            Directory.CreateDirectory(Path.GetDirectoryName(TargetFile));
            File.WriteAllText(TargetFile, frameworkManifest.ToString());
 
            return !Log.HasLoggedErrors;
        }
 
        private bool IsTargetPathIncluded(ITaskItem item)
        {
            return TargetFilePrefixes
                ?.Any(prefix => item.GetMetadata("TargetPath")?.StartsWith(prefix) == true) ?? true;
        }
    }
}