File: GenerateRuntimeDependencies.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.Packaging.csproj (Microsoft.DotNet.Build.Tasks.Packaging)
// 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 Newtonsoft.Json;
using NuGet.RuntimeModel;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Microsoft.DotNet.Build.Tasks.Packaging;

public class GenerateRuntimeDependencies : Task
{
    private const string c_emptyDependency = "none";

    [Required]
    public ITaskItem[] Dependencies
    {
        get;
        set;
    }

    [Required]
    public string PackageId
    {
        get;
        set;
    }

    public ITaskItem RuntimeJsonTemplate
    {
        get;
        set;
    }

    [Required]
    public ITaskItem RuntimeJson
    {
        get;
        set;
    }


    public override bool Execute()
    {
        if (Dependencies == null || Dependencies.Length == 0)
        {
            Log.LogError("Dependencies argument must be specified");
            return false;
        }

        if (String.IsNullOrEmpty(PackageId))
        {
            Log.LogError("PackageID argument must be specified");
            return false;
        }

        if (RuntimeJson == null)
        {
            Log.LogError("RuntimeJson argument must be specified");
            return false;
        }

        string sourceRuntimeFilePath = null;
        if (RuntimeJsonTemplate != null)
        {
            sourceRuntimeFilePath = RuntimeJsonTemplate.GetMetadata("FullPath");
        }
        string destRuntimeFilePath = RuntimeJson.GetMetadata("FullPath");


        Dictionary<string, string> packageAliases = new Dictionary<string, string>();
        foreach (var dependency in Dependencies)
        {
            string alias = dependency.GetMetadata("PackageAlias");

            if (String.IsNullOrEmpty(alias))
            {
                continue;
            }

            Log.LogMessage(MessageImportance.Low, "Aliasing {0} -> {1}", alias, dependency.ItemSpec);
            packageAliases[alias] = dependency.ItemSpec;
        }

        var runtimeGroups = Dependencies.GroupBy(d => d.GetMetadata("TargetRuntime"));

        List<RuntimeDescription> runtimes = new List<RuntimeDescription>();
        foreach (var runtimeGroup in runtimeGroups)
        {
            string targetRuntimeId = runtimeGroup.Key;

            if (String.IsNullOrEmpty(targetRuntimeId))
            {
                Log.LogMessage(MessageImportance.Low, "Skipping dependencies {0} since they don't have a TargetRuntime.", String.Join(", ", runtimeGroup.Select(d => d.ItemSpec)));
                continue;
            }

            if (runtimeGroup.Any(d => d.ItemSpec == c_emptyDependency))
            {
                runtimes.Add(new RuntimeDescription(targetRuntimeId));
                continue;
            }

            List<RuntimeDependencySet> runtimeDependencySets = new List<RuntimeDependencySet>();
            var targetPackageGroups = runtimeGroup.GroupBy(d => GetTargetPackageId(d, packageAliases));
            foreach (var targetPackageGroup in targetPackageGroups)
            {
                string targetPackageId = targetPackageGroup.Key;

                List<RuntimePackageDependency> runtimePackageDependencies = new List<RuntimePackageDependency>();
                var dependencyGroups = targetPackageGroup.GroupBy(d => d.ItemSpec);
                foreach (var dependencyGroup in dependencyGroups)
                {
                    string dependencyId = dependencyGroup.Key;
                    var dependencyVersions = dependencyGroup.Select(d => GetDependencyVersion(d));
                    var maxDependencyVersion = dependencyVersions.Max();
                    runtimePackageDependencies.Add(new RuntimePackageDependency(dependencyId, new VersionRange(maxDependencyVersion)));
                }
                runtimeDependencySets.Add(new RuntimeDependencySet(targetPackageId, runtimePackageDependencies));
            }
            runtimes.Add(new RuntimeDescription(targetRuntimeId, runtimeDependencySets));
        }

        RuntimeGraph runtimeGraph = new RuntimeGraph(runtimes);

        // read in existing JSON, if it was provided so that we preserve any 
        // hand authored #imports or dependencies
        if (!String.IsNullOrEmpty(sourceRuntimeFilePath))
        {
            RuntimeGraph existingGraph  = JsonRuntimeFormat.ReadRuntimeGraph(sourceRuntimeFilePath);
            runtimeGraph = RuntimeGraph.Merge(existingGraph, runtimeGraph);
        }

        string destRuntimeFileDir = Path.GetDirectoryName(destRuntimeFilePath);
        if (!String.IsNullOrEmpty(destRuntimeFileDir) && !Directory.Exists(destRuntimeFileDir))
        {
            Directory.CreateDirectory(destRuntimeFileDir);
        }

        NuGetUtility.WriteRuntimeGraph(destRuntimeFilePath, runtimeGraph);

        return true;
    }

    private string GetTargetPackageId(ITaskItem dependency, IDictionary<string, string> packageAliases)
    {
        string targetPackageId = dependency.GetMetadata("TargetPackage");
        string targetPackageAlias = dependency.GetMetadata("TargetPackageAlias");

        if (!String.IsNullOrEmpty(targetPackageAlias) && !packageAliases.TryGetValue(targetPackageAlias, out targetPackageId))
        {
            Log.LogWarning("Dependency {0} specified TargetPackageAlias {1} but no package was found defining this alias.", dependency.ItemSpec, targetPackageAlias);
        }
        else
        {
            Log.LogMessage(MessageImportance.Low, "Using {0} for TargetPackageAlias {1}", targetPackageId, targetPackageAlias);
        }

        if (String.IsNullOrEmpty(targetPackageId))
        {
            Log.LogMessage(MessageImportance.Low, "Dependency {0} has no parent so will assume {1}.", dependency.ItemSpec, PackageId);
            targetPackageId = PackageId;
        }

        return targetPackageId;
    }

    private NuGetVersion GetDependencyVersion(ITaskItem dependency)
    {
        NuGetVersion dependencyVersion;
        string dependencyVersionString = dependency.GetMetadata("version");

        if (String.IsNullOrEmpty(dependencyVersionString))
        {
            Log.LogWarning("Dependency {0} has no version", dependency.ItemSpec);
            dependencyVersion = new NuGetVersion(0, 0, 0);
        }
        else if (!NuGetVersion.TryParse(dependencyVersionString, out dependencyVersion))
        {
            Log.LogError("Dependency {0} has invalid version {1}", dependency.ItemSpec, dependencyVersionString);
            dependencyVersion = new NuGetVersion(0, 0, 0);
        }

        return dependencyVersion;
    }
}