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

#nullable disable

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

using static Microsoft.DotNet.Cli.EnvironmentVariableNames;

namespace Microsoft.NET.Build.Tasks
{
    [MSBuildMultiThreadableTask]
    public class ResolveTargetingPackAssets : TaskBase, IMultiThreadableTask
    {
        public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

        public ITaskItem[] FrameworkReferences { get; set; } = Array.Empty<ITaskItem>();

        public ITaskItem[] ResolvedTargetingPacks { get; set; } = Array.Empty<ITaskItem>();

        public ITaskItem[] RuntimeFrameworks { get; set; } = Array.Empty<ITaskItem>();

        public bool GenerateErrorForMissingTargetingPacks { get; set; }

        public bool NuGetRestoreSupported { get; set; } = true;

        public bool DisableTransitiveFrameworkReferenceDownloads { get; set; }

        public string NetCoreTargetingPackRoot { get; set; }

        public string ProjectLanguage { get; set; }

        [Output]
        public ITaskItem[] ReferencesToAdd { get; set; }

        [Output]
        public ITaskItem[] AnalyzersToAdd { get; set; }

        [Output]
        public ITaskItem[] PlatformManifests { get; set; }

        [Output]
        public string PackageConflictPreferredPackages { get; set; }

        [Output]
        public ITaskItem[] PackageConflictOverrides { get; set; }

        [Output]
        public ITaskItem[] UsedRuntimeFrameworks { get; set; }

        public ResolveTargetingPackAssets()
        {
        }

        protected override void ExecuteCore()
        {
            StronglyTypedInputs inputs = GetInputs();

            string cacheKey = inputs.CacheKey();

            bool allowCacheLookup = TaskEnvironment.GetEnvironmentVariable(ALLOW_TARGETING_PACK_CACHING) != "0";

            ResolvedAssetsCacheEntry results;

            if (allowCacheLookup && BuildEngine4 is IBuildEngine4 buildEngine4)
            {
                if (buildEngine4.GetRegisteredTaskObject(
                        cacheKey,
                        RegisteredTaskObjectLifetime.AppDomain /* really "until process exit" */)
                    is ResolvedAssetsCacheEntry cacheEntry)
                {
                    // NOTE: It's conceivably possible that the user modified the targeting
                    //       packs between builds. Since that is extremely rare and the standard
                    //       user scenario reads the targeting pack contents over and over without
                    //       modification, we're electing not to check for file modification and
                    //       returning any cached results that we have.

                    results = cacheEntry;
                }
                else
                {
                    results = Resolve(inputs, buildEngine4, allowCacheLookup);
                    buildEngine4.RegisterTaskObject(cacheKey, results, RegisteredTaskObjectLifetime.AppDomain, allowEarlyCollection: true);
                }
            }
            else
            {
                results = Resolve(inputs, BuildEngine4, allowCacheLookup);
            }

            foreach (var error in results.Errors)
            {
                Log.LogError(error);
            }

            ReferencesToAdd = results.ReferencesToAdd;
            AnalyzersToAdd = results.AnalyzersToAdd;
            PlatformManifests = results.PlatformManifests;
            PackageConflictOverrides = results.PackageConflictOverrides;
            PackageConflictPreferredPackages = results.PackageConflictPreferredPackages;
            UsedRuntimeFrameworks = results.UsedRuntimeFrameworks;
        }

        internal StronglyTypedInputs GetInputs() => new(
                        FrameworkReferences,
                        GetResolvedTargetingPacks(),
                        RuntimeFrameworks,
                        GenerateErrorForMissingTargetingPacks,
                        NuGetRestoreSupported,
                        DisableTransitiveFrameworkReferenceDownloads,
                        NetCoreTargetingPackRoot,
                        ProjectLanguage);

        private TargetingPack[] GetResolvedTargetingPacks() => ResolvedTargetingPacks.Select(
            item =>
            {
                string targetingPackPath = item.GetMetadata(MetadataKeys.Path);
                AbsolutePath path = string.IsNullOrEmpty(targetingPackPath)
                    ? default
                    : TaskEnvironment.GetAbsolutePath(targetingPackPath);

                return new TargetingPack(
                    item.ItemSpec,
                    path,
                    item.GetMetadata("TargetingPackFormat"),
                    item.GetMetadata("TargetFramework"),
                    item.GetMetadata("Profile"),
                    item.GetMetadata(MetadataKeys.NuGetPackageId),
                    item.GetMetadata(MetadataKeys.NuGetPackageVersion),
                    item.GetMetadata(MetadataKeys.PackageConflictPreferredPackages));
            }).ToArray();

        private ResolvedAssetsCacheEntry Resolve(StronglyTypedInputs inputs, IBuildEngine4 buildEngine, bool allowCacheLookup)
        {
            List<TaskItem> referencesToAdd = new();
            List<TaskItem> analyzersToAdd = new();
            List<TaskItem> platformManifests = new();
            List<TaskItem> packageConflictOverrides = new();
            List<string> preferredPackages = new();
            List<string> errors = new();

            var resolvedTargetingPacks = inputs.ResolvedTargetingPacks
                .ToDictionary(
                    tp => tp.Name,
                    StringComparer.OrdinalIgnoreCase);

            FrameworkReference[] frameworkReferences = inputs.FrameworkReferences;

            foreach (var frameworkReference in frameworkReferences)
            {
                bool foundTargetingPack = resolvedTargetingPacks.TryGetValue(frameworkReference.Name, out TargetingPack targetingPack);
                AbsolutePath targetingPackRoot = targetingPack?.Path ?? default;

                if (string.IsNullOrEmpty(targetingPackRoot.Value) || !Directory.Exists(targetingPackRoot))
                {
                    if (inputs.GenerateErrorForMissingTargetingPacks)
                    {
                        if (!foundTargetingPack)
                        {
                            if (frameworkReference.Name.Equals("Microsoft.Maui.Essentials", StringComparison.OrdinalIgnoreCase))
                            {
                                errors.Add(Strings.UnknownFrameworkReference_MauiEssentials);
                            }
                            else
                            {
                                errors.Add(string.Format(Strings.UnknownFrameworkReference, frameworkReference.Name));
                            }

                        }
                        else
                        {
                            if (inputs.DisableTransitiveFrameworkReferences)
                            {
                                errors.Add(string.Format(Strings.TargetingPackNotRestored_TransitiveDisabled, frameworkReference.Name));
                            }
                            else if (inputs.NuGetRestoreSupported)
                            {
                                errors.Add(string.Format(Strings.TargetingPackNeedsRestore, frameworkReference.Name));
                            }
                            else
                            {
                                errors.Add(string.Format(
                                    Strings.TargetingApphostPackMissingCannotRestore,
                                    "Targeting",
                                    $"{inputs.NetCoreTargetingPackRoot}\\{targetingPack.NuGetPackageId ?? ""}",
                                    targetingPack.TargetFramework ?? "",
                                    targetingPack.NuGetPackageId ?? "",
                                    targetingPack.NuGetPackageVersion ?? ""
                                    ));
                            }
                        }
                    }
                }
                else
                {
                    string targetingPackFormat = targetingPack.Format;

                    if (targetingPackFormat.Equals("NETStandardLegacy", StringComparison.OrdinalIgnoreCase))
                    {
                        AddNetStandardTargetingPackAssets(targetingPack, targetingPackRoot, referencesToAdd);
                    }
                    else
                    {
                        string targetingPackTargetFramework = targetingPack.TargetFramework;
                        if (string.IsNullOrEmpty(targetingPackTargetFramework))
                        {
                            targetingPackTargetFramework = "netcoreapp3.0";
                        }

                        AbsolutePath targetingPackDllFolder = TaskEnvironment.GetAbsolutePath(
                            Path.Combine(targetingPackRoot.OriginalValue, "ref", targetingPackTargetFramework));

                        //  Fall back to netcoreapp5.0 folder if looking for net5.0 and it's not found
                        if (!Directory.Exists(targetingPackDllFolder) &&
                            targetingPackTargetFramework.Equals("net5.0", StringComparison.OrdinalIgnoreCase))
                        {
                            targetingPackTargetFramework = "netcoreapp5.0";
                            targetingPackDllFolder = TaskEnvironment.GetAbsolutePath(
                                Path.Combine(targetingPackRoot.OriginalValue, "ref", targetingPackTargetFramework));
                        }

                        string targetingPackOriginalDataPath = Path.Combine(targetingPackRoot.OriginalValue, "data");

                        AbsolutePath platformManifestPath = TaskEnvironment.GetAbsolutePath(
                            Path.Combine(targetingPackOriginalDataPath, "PlatformManifest.txt"));

                        AbsolutePath packageOverridesPath = TaskEnvironment.GetAbsolutePath(
                            Path.Combine(targetingPackOriginalDataPath, "PackageOverrides.txt"));

                        AbsolutePath frameworkListPath = TaskEnvironment.GetAbsolutePath(
                            Path.Combine(targetingPackOriginalDataPath, "FrameworkList.xml"));

                        FrameworkListDefinition definition = new(
                            frameworkListPath: frameworkListPath,
                            targetingPackRoot: targetingPackRoot,
                            targetingPackDllFolder: targetingPackDllFolder,
                            targetingPack.Name,
                            targetingPack.Profile,
                            targetingPack.NuGetPackageId,
                            targetingPack.NuGetPackageVersion,
                            inputs.ProjectLanguage);

                        AddItemsFromFrameworkList(definition, buildEngine, referencesToAdd, analyzersToAdd, allowCacheLookup);

                        if (File.Exists(platformManifestPath))
                        {
                            platformManifests.Add(new TaskItem(platformManifestPath.OriginalValue));
                        }

                        if (File.Exists(packageOverridesPath))
                        {
                            packageConflictOverrides.Add(CreatePackageOverride(targetingPack.NuGetPackageId, packageOverridesPath));
                        }

                        preferredPackages.AddRange(targetingPack.PackageConflictPreferredPackages.Split(';'));
                    }
                }
            }

            //  Calculate which RuntimeFramework items should actually be used based on framework references
            HashSet<string> frameworkReferenceNames = new(frameworkReferences.Select(fr => fr.Name), StringComparer.OrdinalIgnoreCase);

            //  Filter out duplicate references (which can happen when referencing two different profiles that overlap)
            List<TaskItem> deduplicatedReferences = DeduplicateItems(referencesToAdd);
            List<TaskItem> deduplicatedAnalyzers = DeduplicateItems(analyzersToAdd);

            ResolvedAssetsCacheEntry newCacheEntry = new()
            {
                ReferencesToAdd = deduplicatedReferences.Distinct().ToArray(),
                AnalyzersToAdd = deduplicatedAnalyzers.Distinct().ToArray(),
                PlatformManifests = platformManifests.ToArray(),
                PackageConflictOverrides = packageConflictOverrides.ToArray(),
                PackageConflictPreferredPackages = string.Join(";", preferredPackages),
                UsedRuntimeFrameworks = inputs.RuntimeFrameworks.Where(rf => frameworkReferenceNames.Contains(rf.FrameworkName))
                    .Select(rf => rf.Item)
                    .ToArray(),
                Errors = errors.ToArray(),
            };
            return newCacheEntry;
        }

        //  Get distinct items based on case-insensitive ItemSpec comparison
        private static List<TaskItem> DeduplicateItems(List<TaskItem> items)
        {
            HashSet<string> seenItemSpecs = new(StringComparer.OrdinalIgnoreCase);
            List<TaskItem> deduplicatedItems = new(items.Count);
            foreach (var item in items)
            {
                if (seenItemSpecs.Add(item.ItemSpec))
                {
                    deduplicatedItems.Add(item);
                }
            }
            return deduplicatedItems;
        }

        private static TaskItem CreatePackageOverride(string runtimeFrameworkName, AbsolutePath packageOverridesPath)
        {
            TaskItem packageOverride = new(runtimeFrameworkName);
            packageOverride.SetMetadata("OverriddenPackages", File.ReadAllText(packageOverridesPath));
            return packageOverride;
        }

        private void AddNetStandardTargetingPackAssets(TargetingPack targetingPack, AbsolutePath targetingPackRoot, List<TaskItem> referencesToAdd)
        {
            string targetingPackTargetFramework = targetingPack.TargetFramework;
            AbsolutePath targetingPackAssetPath = TaskEnvironment.GetAbsolutePath(
                Path.Combine(targetingPackRoot.OriginalValue, "build", targetingPackTargetFramework, "ref"));

            foreach (var dll in Directory.GetFiles(targetingPackAssetPath, "*.dll"))
            {
                string itemSpec = Path.Combine(targetingPackAssetPath.OriginalValue, Path.GetFileName(dll));

                var reference = CreateItem(
                    itemSpec,
                    targetingPack.Name,
                    targetingPack.NuGetPackageId,
                    targetingPack.NuGetPackageVersion);

                if (!Path.GetFileName(dll).Equals("netstandard.dll", StringComparison.OrdinalIgnoreCase))
                {
                    reference.SetMetadata("Facade", "true");
                }

                referencesToAdd.Add(reference);
            }
        }

        private static void AddItemsFromFrameworkList(FrameworkListDefinition definition, IBuildEngine4 buildEngine4, List<TaskItem> referenceItems, List<TaskItem> analyzerItems, bool allowCacheLookup)
        {
            string frameworkListKey = definition.CacheKey();

            if (allowCacheLookup && buildEngine4 is not null)
            {
                if (buildEngine4.GetRegisteredTaskObject(
                        frameworkListKey,
                        RegisteredTaskObjectLifetime.AppDomain)
                    is FrameworkList cacheEntry)
                {
                    // As above, we are not even checking timestamps here
                    // and instead assuming that the targeting pack folder
                    // is fully immutable.

                    analyzerItems.AddRange(cacheEntry.Analyzers);
                    referenceItems.AddRange(cacheEntry.References);
                    return;
                }

                FrameworkList list = ReadFrameworkList(definition);
                buildEngine4.RegisterTaskObject(frameworkListKey, list, RegisteredTaskObjectLifetime.AppDomain, allowEarlyCollection: true);

                analyzerItems.AddRange(list.Analyzers);
                referenceItems.AddRange(list.References);
                return;
            }

            FrameworkList uncachedList = ReadFrameworkList(definition);

            analyzerItems.AddRange(uncachedList.Analyzers);
            referenceItems.AddRange(uncachedList.References);
        }

        private static FrameworkList ReadFrameworkList(FrameworkListDefinition definition)
        {
            XDocument frameworkListDoc = XDocument.Load(definition.FrameworkListPath);

            string profile = definition.Profile;

            bool usePathElementsInFrameworkListAsFallBack =
                TestFirstFileInFrameworkListUsingAssemblyNameConvention(definition.TargetingPackDllFolder, frameworkListDoc);

            List<TaskItem> referenceItemsFromThisFramework = new();
            List<TaskItem> analyzerItemsFromThisFramework = new();

            foreach (var fileElement in frameworkListDoc.Root.Elements("File"))
            {
                if (!string.IsNullOrEmpty(profile))
                {
                    var profileAttributeValue = fileElement.Attribute("Profile")?.Value;

                    if (profileAttributeValue == null)
                    {
                        //  If profile was specified but this assembly doesn't belong to any profiles, don't reference it
                        continue;
                    }

                    var assemblyProfiles = profileAttributeValue.Split(';');
                    if (!assemblyProfiles.Contains(profile, StringComparer.OrdinalIgnoreCase))
                    {
                        //  Assembly wasn't in profile specified, so don't reference it
                        continue;
                    }
                }

                string referencedByDefaultAttributeValue = fileElement.Attribute("ReferencedByDefault")?.Value;
                if (referencedByDefaultAttributeValue != null &&
                    referencedByDefaultAttributeValue.Equals("false", StringComparison.OrdinalIgnoreCase))
                {
                    //  Don't automatically reference this assembly if it has ReferencedByDefault="false"
                    continue;
                }

                string itemType = fileElement.Attribute("Type")?.Value;
                bool isAnalyzer = itemType?.Equals("Analyzer", StringComparison.OrdinalIgnoreCase) ?? false;

                string assemblyName = fileElement.Attribute("AssemblyName").Value;

                string dllPath = usePathElementsInFrameworkListAsFallBack || isAnalyzer ?
                    Path.Combine(definition.TargetingPackRoot.OriginalValue, fileElement.Attribute("Path").Value) :
                    GetDllPathViaAssemblyName(definition.TargetingPackDllFolder.OriginalValue, assemblyName);

                var item = CreateItem(dllPath, definition.FrameworkReferenceName, definition.NuGetPackageId, definition.NuGetPackageVersion);

                item.SetMetadata("AssemblyName", assemblyName);
                item.SetMetadata("AssemblyVersion", fileElement.Attribute("AssemblyVersion").Value);
                item.SetMetadata("FileVersion", fileElement.Attribute("FileVersion").Value);
                item.SetMetadata("PublicKeyToken", fileElement.Attribute("PublicKeyToken").Value);

                if (isAnalyzer)
                {
                    string itemLanguage = fileElement.Attribute("Language")?.Value;

                    if (itemLanguage != null)
                    {
                        // expect cs instead of C#, fs rather than F# per NuGet conventions
                        string projectLanguage = definition.ProjectLanguage?.Replace('#', 's');

                        if (projectLanguage == null || !projectLanguage.Equals(itemLanguage, StringComparison.OrdinalIgnoreCase))
                        {
                            continue;
                        }
                    }

                    analyzerItemsFromThisFramework.Add(item);
                }
                else
                {
                    referenceItemsFromThisFramework.Add(item);
                }
            }

            return new FrameworkList
            {
                Analyzers = analyzerItemsFromThisFramework.ToArray(),
                References = referenceItemsFromThisFramework.ToArray(),
            };
        }

        /// <summary>
        /// Due to https://github.com/dotnet/sdk/issues/12098 we fall back to use "Path" when "AssemblyName" will
        /// not resolve the actual dll.
        /// </summary>
        /// <returns>if use we should use "Path" element in frameworkList as a fallback</returns>
        private static bool TestFirstFileInFrameworkListUsingAssemblyNameConvention(AbsolutePath targetingPackDllFolder,
            XDocument frameworkListDoc)
        {
            bool usePathElementsInFrameworkListPathAsFallBack;
            var firstFileElement = frameworkListDoc.Root.Elements("File").FirstOrDefault();
            if (firstFileElement == null)
            {
                usePathElementsInFrameworkListPathAsFallBack = false;
            }
            else
            {
                string assemblyName = firstFileElement.Attribute("AssemblyName").Value;
                string dllPath = Path.Combine(targetingPackDllFolder, assemblyName + ".dll");

                usePathElementsInFrameworkListPathAsFallBack = !File.Exists(dllPath);
            }

            return usePathElementsInFrameworkListPathAsFallBack;
        }

        private static string GetDllPathViaAssemblyName(string targetingPackDllFolder, string assemblyName)
        {
            var dllPath = Path.Combine(targetingPackDllFolder, assemblyName + ".dll");
            return dllPath;
        }

        private static TaskItem CreateItem(string dll, ITaskItem targetingPack)
        {
            return CreateItem(
                dll,
                targetingPack.ItemSpec,
                targetingPack.GetMetadata(MetadataKeys.NuGetPackageId),
                targetingPack.GetMetadata(MetadataKeys.NuGetPackageVersion));
        }

        private static TaskItem CreateItem(string dll, string frameworkReferenceName, string nuGetPackageId, string nuGetPackageVersion)
        {
            var reference = new TaskItem(dll);

            reference.SetMetadata(MetadataKeys.ExternallyResolved, "true");
            reference.SetMetadata(MetadataKeys.Private, "false");
            reference.SetMetadata(MetadataKeys.NuGetPackageId, nuGetPackageId);
            reference.SetMetadata(MetadataKeys.NuGetPackageVersion, nuGetPackageVersion);

            reference.SetMetadata("FrameworkReferenceName", frameworkReferenceName);
            reference.SetMetadata("FrameworkReferenceVersion", nuGetPackageVersion);

            return reference;
        }

        internal class StronglyTypedInputs
        {
            public FrameworkReference[] FrameworkReferences { get; private set; }
            public TargetingPack[] ResolvedTargetingPacks { get; private set; }
            public RuntimeFramework[] RuntimeFrameworks { get; private set; }
            public bool GenerateErrorForMissingTargetingPacks { get; private set; }
            public bool NuGetRestoreSupported { get; private set; }
            public bool DisableTransitiveFrameworkReferences { get; private set; }
            public string NetCoreTargetingPackRoot { get; private set; }
            public string ProjectLanguage { get; private set; }

            public StronglyTypedInputs(
                ITaskItem[] frameworkReferences,
                TargetingPack[] resolvedTargetingPacks,
                ITaskItem[] runtimeFrameworks,
                bool generateErrorForMissingTargetingPacks,
                bool nuGetRestoreSupported,
                bool disableTransitiveFrameworkReferences,
                string netCoreTargetingPackRoot,
                string projectLanguage)
            {
                FrameworkReferences = frameworkReferences.Select(fr => new FrameworkReference(fr.ItemSpec)).ToArray();
                ResolvedTargetingPacks = resolvedTargetingPacks;
                RuntimeFrameworks = runtimeFrameworks.Select(item => new RuntimeFramework(item.ItemSpec, item.GetMetadata(MetadataKeys.FrameworkName), item)).ToArray();
                GenerateErrorForMissingTargetingPacks = generateErrorForMissingTargetingPacks;
                NuGetRestoreSupported = nuGetRestoreSupported;
                DisableTransitiveFrameworkReferences = disableTransitiveFrameworkReferences;
                NetCoreTargetingPackRoot = netCoreTargetingPackRoot;
                ProjectLanguage = projectLanguage;
            }

            public string CacheKey()
            {
                StringBuilder cacheKeyBuilder = new(nameof(ResolveTargetingPackAssets) + nameof(StronglyTypedInputs));
                cacheKeyBuilder.AppendLine();

                foreach (var frameworkReference in FrameworkReferences)
                {
                    cacheKeyBuilder.AppendLine(frameworkReference.CacheKey());
                }
                cacheKeyBuilder.AppendLine();
                foreach (var resolvedTargetingPack in ResolvedTargetingPacks)
                {
                    cacheKeyBuilder.AppendLine(resolvedTargetingPack.CacheKey());
                }

                cacheKeyBuilder.AppendLine(nameof(RuntimeFrameworks));
                foreach (var runtimeFramework in RuntimeFrameworks)
                {
                    cacheKeyBuilder.AppendLine(runtimeFramework.CacheKey());
                }

                cacheKeyBuilder.AppendLine($"{nameof(GenerateErrorForMissingTargetingPacks)}={GenerateErrorForMissingTargetingPacks}");
                cacheKeyBuilder.AppendLine($"{nameof(NuGetRestoreSupported)}={NuGetRestoreSupported}");
                cacheKeyBuilder.AppendLine($"{nameof(DisableTransitiveFrameworkReferences)}={DisableTransitiveFrameworkReferences}");

                cacheKeyBuilder.AppendLine($"{nameof(NetCoreTargetingPackRoot)}={NetCoreTargetingPackRoot}");

                cacheKeyBuilder.AppendLine($"{nameof(ProjectLanguage)}={ProjectLanguage}");

                return cacheKeyBuilder.ToString();

            }
        }

        private class FrameworkList
        {
            public IReadOnlyList<TaskItem> Analyzers;
            public IReadOnlyList<TaskItem> References;
        }

        internal class FrameworkReference
        {
            public string Name { get; private set; }

            public FrameworkReference(string name)
            {
                Name = name;
            }

            public string CacheKey()
            {
                return $"FrameworkReference: {Name}";
            }
        }

        internal class TargetingPack
        {
            public string Name { get; private set; }
            public AbsolutePath Path { get; private set; }
            public string Format { get; private set; }
            public string TargetFramework { get; private set; }
            public string Profile { get; private set; }
            public string NuGetPackageId { get; private set; }
            public string NuGetPackageVersion { get; private set; }
            public string PackageConflictPreferredPackages { get; private set; }

            public TargetingPack(
                string name,
                AbsolutePath path,
                string format,
                string targetFramework,
                string profile,
                string nuGetPackageId,
                string nuGetPackageVersion,
                string packageConflictPreferredPackages)
            {
                Name = name;
                Path = path;
                Format = format;
                TargetFramework = targetFramework;
                Profile = profile;
                NuGetPackageId = nuGetPackageId;
                NuGetPackageVersion = nuGetPackageVersion;
                PackageConflictPreferredPackages = packageConflictPreferredPackages;
            }

            public string CacheKey()
            {
                // AbsolutePath carries both the resolved (Value) and user-input (OriginalValue) forms;
                // both contribute to outputs, so both must be in the key.
                StringBuilder builder = new();
                builder.AppendLine(nameof(TargetingPack));

                builder.AppendLine(Name);
                builder.AppendLine(Path.Value);
                builder.AppendLine(Path.OriginalValue);
                builder.AppendLine(Format);
                builder.AppendLine(TargetFramework);
                builder.AppendLine(Profile);
                builder.AppendLine(NuGetPackageId);
                builder.AppendLine(NuGetPackageVersion);
                builder.AppendLine(PackageConflictPreferredPackages);

                return builder.ToString();
            }
        }

        internal class RuntimeFramework
        {
            public string Name { get; private set; }
            public string FrameworkName { get; private set; }
            public readonly ITaskItem Item;

            public RuntimeFramework(string name, string frameworkName, ITaskItem item)
            {
                Name = name;
                FrameworkName = frameworkName;
                Item = item;
            }

            public string CacheKey()
            {
                StringBuilder builder = new();
                builder.AppendLine(nameof(RuntimeFramework));
                builder.AppendLine(Name);
                builder.AppendLine(FrameworkName);

                if (Item is not null)
                {
                    builder.AppendLine(Item.ItemSpec);

                    foreach (string metadataName in Item.MetadataNames.Cast<string>().OrderBy(name => name, StringComparer.OrdinalIgnoreCase))
                    {
                        builder.Append(metadataName);
                        builder.Append('=');
                        builder.AppendLine(Item.GetMetadata(metadataName));
                    }
                }

                return builder.ToString();
            }
        }

        internal readonly struct FrameworkListDefinition
        {
            public readonly AbsolutePath FrameworkListPath;
            public readonly AbsolutePath TargetingPackRoot;
            public readonly AbsolutePath TargetingPackDllFolder;
            public readonly string ProjectLanguage;

            public readonly string FrameworkReferenceName;
            public readonly string Profile;
            public readonly string NuGetPackageId;
            public readonly string NuGetPackageVersion;

            public FrameworkListDefinition(AbsolutePath frameworkListPath,
                                           AbsolutePath targetingPackRoot,
                                           AbsolutePath targetingPackDllFolder,
                                           string frameworkReferenceName,
                                           string profile,
                                           string nuGetPackageId,
                                           string nuGetPackageVersion,
                                           string projectLanguage)
            {
                FrameworkListPath = frameworkListPath;
                TargetingPackRoot = targetingPackRoot;
                TargetingPackDllFolder = targetingPackDllFolder;
                ProjectLanguage = projectLanguage;

                FrameworkReferenceName = frameworkReferenceName;
                Profile = profile;
                NuGetPackageId = nuGetPackageId;
                NuGetPackageVersion = nuGetPackageVersion;
            }

            /// <summary>
            /// Construct a key for the framework-specific cache lookup.
            /// </summary>
            public string CacheKey()
            {
                // IMPORTANT: any input changes that can affect the output should be included in this key.
                // AbsolutePath carries both the resolved (Value) and user-input (OriginalValue) forms;
                // both contribute to outputs, so both must be in the key.
                StringBuilder keyBuilder = new(nameof(FrameworkListDefinition));
                keyBuilder.AppendLine();
                keyBuilder.AppendLine(FrameworkListPath.Value);
                keyBuilder.AppendLine(FrameworkListPath.OriginalValue);
                keyBuilder.AppendLine(TargetingPackRoot.Value);
                keyBuilder.AppendLine(TargetingPackRoot.OriginalValue);
                keyBuilder.AppendLine(TargetingPackDllFolder.Value);
                keyBuilder.AppendLine(TargetingPackDllFolder.OriginalValue);
                keyBuilder.AppendLine(FrameworkReferenceName);
                keyBuilder.AppendLine(Profile);
                keyBuilder.AppendLine(NuGetPackageId);
                keyBuilder.AppendLine(NuGetPackageVersion);
                keyBuilder.AppendLine(ProjectLanguage);

                string frameworkListKey = keyBuilder.ToString();
                return frameworkListKey;
            }
        }

        private class ResolvedAssetsCacheEntry
        {
            public ITaskItem[] ReferencesToAdd;
            public ITaskItem[] AnalyzersToAdd;
            public ITaskItem[] PlatformManifests;
            public string PackageConflictPreferredPackages;
            public ITaskItem[] PackageConflictOverrides;
            public ITaskItem[] UsedRuntimeFrameworks;
            public string[] Errors;
        }
    }
}