File: OverrideAndCreateBundledNETCoreAppPackageVersion.cs
Web Access
Project: sdk-tasks.csproj (sdk-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 NuGet.Frameworks;
using NuGet.Versioning;

namespace Microsoft.DotNet.Build.Tasks
{
    /// <summary>
    /// For stage 2, we want use the version numbers from stage 0 for the downlevel .NET versions.  This is because
    /// the latest patches of different major versions are built entirely separately, and we want to have tests
    /// on downlevel versions but we can't depend on the latest patches being available in test environments.
    ///
    /// So we copy the version numbers from stage 0 for those downlevel versions.  This also applies to the downlevel
    /// runtime pack versions in the Mono toolchain workload manifest, which override the bundled versions for
    /// browser-wasm and mobile projects.
    ///
    /// However, if the stage 2 version is a preview version, we don't overwrite it.  This is because in this case
    /// the version of .NET hasn't been released yet, so we want to use the later preview version.  The preview
    /// versions should all be on the same NuGet feeds anyway.
    /// </summary>
    public sealed class OverrideAndCreateBundledNETCoreAppPackageVersion : Task
    {
        [Required] public string Stage0BundledVersionsPath { get; set; }
        [Required] public string Stage2BundledVersionsPath { get; set; }

        /// <summary>
        /// WorkloadManifest.targets files of the microsoft.net.workload.mono.toolchain.current manifest in stage 0.
        /// The one with the highest manifest version is used.
        /// </summary>
        public ITaskItem[] Stage0MonoToolchainManifestTargetsPaths { get; set; } = Array.Empty<ITaskItem>();

        /// <summary>
        /// WorkloadManifest.targets files of the microsoft.net.workload.mono.toolchain.current manifest in stage 2.
        /// The Mono workload targets override the runtime/targeting/WebAssembly SDK pack versions for downlevel
        /// browser-wasm and mobile projects, so their downlevel versions are replaced with the stage 0 ones as well.
        /// </summary>
        public ITaskItem[] Stage2MonoToolchainManifestTargetsPaths { get; set; } = Array.Empty<ITaskItem>();

        public override bool Execute()
        {
            try
            {
                var stage0Doc = XDocument.Load(Stage0BundledVersionsPath);
                var stage2Doc = XDocument.Load(Stage2BundledVersionsPath);
                var ns = stage2Doc.Root.Name.Namespace;

                // Load all items from all ItemGroups
                var items2 = stage2Doc.Root.Elements(ns + "ItemGroup").SelectMany(ig => ig.Elements()).ToList();
                var items0 = stage0Doc.Root.Elements(ns + "ItemGroup").SelectMany(ig => ig.Elements()).ToList();

                // Find latest TargetFramework using NuGetFramework
                var allTargetFrameworks = items2
                    .Select(e => e.Attribute("TargetFramework")?.Value)
                    .Where(v => !string.IsNullOrEmpty(v))
                    .Distinct()
                    .Select(tf => new { Raw = tf, Parsed = NuGetFramework.Parse(tf) })
                    .ToList();
                var latest = allTargetFrameworks
                    .OrderByDescending(tf => tf.Parsed.Version)
                    .FirstOrDefault();
                var latestTargetFramework = latest?.Raw;

                // Helper for matching and updating
                void UpdateItems(string elementName, string[] matchAttrs, string[] updateAttrs)
                {
                    var items2Filtered = items2
                        .Where(e => e.Name.LocalName == elementName && e.Attribute("TargetFramework")?.Value != latestTargetFramework)
                        .ToList();
                    foreach (var item2 in items2Filtered)
                    {
                        var matches0 = items0
                            .Where(e => e.Name.LocalName == elementName && matchAttrs.All(attr => (e.Attribute(attr)?.Value ?? "") == (item2.Attribute(attr)?.Value ?? "")))
                            .ToList();
                        if (matches0.Count == 0)
                        {
                            Log.LogError($"No matching {elementName} in stage 0 for: {string.Join(", ", matchAttrs.Select(a => $"{a}={item2.Attribute(a)?.Value}"))}");
                            continue;
                        }
                        if (matches0.Count > 1)
                        {
                            Log.LogError($"Multiple matches for {elementName} in stage 0 for: {string.Join(", ", matchAttrs.Select(a => $"{a}={item2.Attribute(a)?.Value}"))}");
                            continue;
                        }
                        var item0 = matches0[0];
                        foreach (var updateAttr in updateAttrs)
                        {
                            var v0 = item0.Attribute(updateAttr)?.Value;
                            var v2 = item2.Attribute(updateAttr)?.Value;
                            if (v0 != null && v2 != v0)
                            {
                                bool isPreview = false;
                                if (!string.IsNullOrEmpty(v2))
                                {
                                    try
                                    {
                                        var nugetVersion = NuGetVersion.Parse(v2);
                                        isPreview = nugetVersion.IsPrerelease;
                                    }
                                    catch
                                    {
                                        // If parsing fails, treat as non-preview
                                        isPreview = false;
                                    }
                                }
                                if (!isPreview)
                                {
                                    item2.SetAttributeValue(updateAttr, v0);
                                }
                            }
                        }
                        // Log if other metadata differs
                        foreach (var attr in item2.Attributes())
                        {
                            if (matchAttrs.Contains(attr.Name.LocalName) || updateAttrs.Contains(attr.Name.LocalName))
                                continue;
                            var v0 = item0.Attribute(attr.Name)?.Value;
                            if (v0 != null && v0 != attr.Value)
                                Log.LogMessage(MessageImportance.Low, $"{elementName} {string.Join(", ", matchAttrs.Select(a => $"{a}={item2.Attribute(a)?.Value}"))}: Metadata '{attr.Name}' differs: stage0='{v0}', stage2='{attr.Value}'");
                        }
                    }
                }

                UpdateItems("KnownFrameworkReference", new[] { "Include", "TargetFramework" }, new[] { "LatestRuntimeFrameworkVersion", "TargetingPackVersion" });
                UpdateItems("KnownAppHostPack", new[] { "Include", "TargetFramework" }, new[] { "AppHostPackVersion" });
                UpdateItems("KnownCrossgen2Pack", new[] { "Include", "TargetFramework" }, new[] { "Crossgen2PackVersion" });
                UpdateItems("KnownILCompilerPack", new[] { "Include", "TargetFramework" }, new[] { "ILCompilerPackVersion" });
                UpdateItems("KnownRuntimePack", new[] { "Include", "TargetFramework", "RuntimePackLabels" }, new[] { "LatestRuntimeFrameworkVersion" });
                UpdateItems("KnownILLinkPack", new[] { "Include", "TargetFramework" }, new[] { "ILLinkPackVersion" });
                UpdateItems("KnownAspNetCorePack", new[] { "Include", "TargetFramework" }, new[] { "AspNetCorePackVersion" });

                stage2Doc.Save(Stage2BundledVersionsPath);

                OverrideMonoToolchainManifestVersions();

                return !Log.HasLoggedErrors;
            }
            catch (Exception ex)
            {
                Log.LogErrorFromException(ex, true);
                return false;
            }
        }

        private void OverrideMonoToolchainManifestVersions()
        {
            if (Stage0MonoToolchainManifestTargetsPaths.Length == 0 || Stage2MonoToolchainManifestTargetsPaths.Length == 0)
            {
                return;
            }

            // NetTFMUpdate: Remove this downlevel override once stage 0 Mono workloads include net12 runtime pack versions.
            // e.g. sdk-manifests/11.0.100-rc.1/microsoft.net.workload.mono.toolchain.current/11.0.100-rc.1.26425.128/WorkloadManifest.targets
            var stage0Path = Stage0MonoToolchainManifestTargetsPaths
                .Select(i => i.ItemSpec)
                .OrderByDescending(p => NuGetVersion.TryParse(Path.GetFileName(Path.GetDirectoryName(p)), out var v) ? v : new NuGetVersion(0, 0, 0))
                .First();
            var stage0Versions = LoadDownlevelRuntimePackVersions(XDocument.Load(stage0Path))
                .ToDictionary(e => e.Name.LocalName, e => e.Value.Trim());

            foreach (var stage2Path in Stage2MonoToolchainManifestTargetsPaths.Select(i => i.ItemSpec))
            {
                var stage2Doc = XDocument.Load(stage2Path, LoadOptions.PreserveWhitespace);
                bool changed = false;
                foreach (var property in LoadDownlevelRuntimePackVersions(stage2Doc))
                {
                    if (stage0Versions.TryGetValue(property.Name.LocalName, out var v0) &&
                        v0 != property.Value.Trim() &&
                        NuGetVersion.TryParse(property.Value.Trim(), out var v2) && !v2.IsPrerelease)
                    {
                        Log.LogMessage(MessageImportance.Low, $"{stage2Path}: overriding {property.Name.LocalName} '{property.Value}' with stage 0 value '{v0}'.");
                        property.Value = v0;
                        changed = true;
                    }
                }

                if (changed)
                {
                    stage2Doc.Save(stage2Path, SaveOptions.DisableFormatting);
                }
            }
        }

        // _RuntimePackInWorkloadVersion6, _RuntimePackInWorkloadVersion10, etc. but not _RuntimePackInWorkloadVersionCurrent.
        private static IEnumerable<XElement> LoadDownlevelRuntimePackVersions(XDocument doc)
        {
            const string Prefix = "_RuntimePackInWorkloadVersion";
            return doc.Root.Elements(doc.Root.Name.Namespace + "PropertyGroup")
                .SelectMany(pg => pg.Elements())
                .Where(e => e.Name.LocalName.StartsWith(Prefix, StringComparison.Ordinal) &&
                            e.Name.LocalName.Length > Prefix.Length &&
                            e.Name.LocalName.Substring(Prefix.Length).All(char.IsDigit))
                .ToList();
        }
    }
}