File: GetLastStablePackage.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 NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.DotNet.Build.Tasks.Packaging;

public class GetLastStablePackage : Microsoft.Build.Utilities.Task
{
    /// <summary>
    /// List of packages to look up.
    /// The closest StablePackage version that is less than the version of each of these packages will be returned in LastStablePackages.
    ///   Identity: Package ID
    ///   Version: Package version.
    /// </summary>
    [Required]
    public ITaskItem[] LatestPackages { get; set; }

    /// <summary>
    /// List of previously shipped packages.
    ///   Identity: Package ID
    ///   Version: Package version.
    /// </summary>
    public ITaskItem[] StablePackages { get; set; }

    /// <summary>
    /// Package index files used to define stable packages.
    /// </summary>
    public ITaskItem[] PackageIndexes { get; set; }

    /// <summary>
    /// <see langword="true"/> if the result version can be a version from the same release.
    /// <see langword="false"/> otherwise. Defaults to false.
    /// </summary>
    public bool DoNotAllowVersionsFromSameRelease { get; set; }
    

    /// <summary>
    /// Latest version from StablePackages for all packages in LatestPackages.
    /// If a version isn't found for an item in LatestPackage that will not be included in this set.
    /// </summary>
    [Output]
    public ITaskItem[] LastStablePackages { get; set; }

    public override bool Execute()
    {
        if (LatestPackages == null || LatestPackages.Length == 0)
        {
            return true;
        }

        if (PackageIndexes != null && PackageIndexes.Length > 0)
        {
            GetLastStablePackagesFromIndex();
        }
        else
        {
            GetLastStablePackagesFromStablePackages();
        }

        return !Log.HasLoggedErrors;
    }

    public void GetLastStablePackagesFromStablePackages()
    {
        Dictionary<string, ITaskItem> originalItems = new Dictionary<string, ITaskItem>();
        Dictionary<string, Version> latestPackages = new Dictionary<string, Version>();
        Dictionary<string, Version> lastStablePackages = new Dictionary<string, Version>();

        foreach (var latestPackage in LatestPackages)
        {
            var packageId = latestPackage.ItemSpec;

            var versionString = latestPackage.GetMetadata("Version");
            NuGetVersion nuGetVersion = null;
            if (versionString == null || !NuGetVersion.TryParse(versionString, out nuGetVersion))
            {
                Log.LogMessage($"Could not parse version {versionString} for LatestPackage {packageId}, will use latest stable.");
            }

            latestPackages[packageId] = nuGetVersion?.Version;
            originalItems[packageId] = latestPackage;
        }

        foreach (var stablePackage in StablePackages.NullAsEmpty())
        {
            var packageId = stablePackage.ItemSpec;

            Version latestVersion;
            if (!latestPackages.TryGetValue(packageId, out latestVersion))
            {
                continue;
            }

            var versionString = stablePackage.GetMetadata("Version");
            Version stableVersion;
            if (versionString == null || !Version.TryParse(versionString, out stableVersion))
            {
                Log.LogError($"Could not parse version {versionString} for StablePackage {packageId}");
                continue;
            }
            stableVersion = VersionUtility.As4PartVersion(stableVersion);

            // only consider a stable version less or equal to than current version
            if (latestVersion != null && stableVersion >= latestVersion)
            {
                continue;
            }

            Version lastStableVersion;
            if (!lastStablePackages.TryGetValue(packageId, out lastStableVersion) || lastStableVersion < stableVersion)
            {
                lastStablePackages[packageId] = stableVersion;
            }
        }

        LastStablePackages = lastStablePackages.Select(p => CreateItem(originalItems[p.Key], p.Value)).ToArray();
    }

    public void GetLastStablePackagesFromIndex()
    {
        var index = PackageIndex.Load(PackageIndexes.Select(pi => pi.GetMetadata("FullPath")));

        List<ITaskItem> lastStablePackages = new List<ITaskItem>();

        foreach (var latestPackage in LatestPackages)
        {
            var packageId = latestPackage.ItemSpec;

            var versionString = latestPackage.GetMetadata("Version");
            NuGetVersion nuGetVersion = null;
            if (versionString == null || !NuGetVersion.TryParse(versionString, out nuGetVersion))
            {
                Log.LogMessage($"Could not parse version {versionString} for LatestPackage {packageId}, will use latest stable.");
            }

            var latestVersion = (DoNotAllowVersionsFromSameRelease) ? VersionUtility.As2PartVersion(nuGetVersion?.Version) : nuGetVersion?.Version;

            PackageInfo info;
            if (index.Packages.TryGetValue(packageId, out info))
            {
                IEnumerable<Version> candidateVersions = (latestVersion == null) ? info.StableVersions : info.StableVersions.Where(sv => VersionUtility.As4PartVersion(sv) < latestVersion);
                if (candidateVersions.Any())
                {
                    lastStablePackages.Add(CreateItem(latestPackage, candidateVersions.Max()));
                }
            }
        }

        LastStablePackages = lastStablePackages.ToArray();

    }

    private ITaskItem CreateItem(ITaskItem originalItem, Version version)
    {
        var item = new TaskItem(originalItem);
        item.SetMetadata("Version", version.ToString(3));
        return item;
    }
}