File: src\CheckRequiredDotNetVersion.cs
Web Access
Project: Microsoft.DotNet.Arcade.Sdk.csproj (Microsoft.DotNet.Arcade.Sdk)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Versioning;

namespace Microsoft.DotNet.Arcade.Sdk;

[MSBuildMultiThreadableTask]
public class CheckRequiredDotNetVersion : Task, IMultiThreadableTask
{
    private readonly record struct CacheKey(AbsolutePath GlobalJsonPath, string SdkVersion, DateTime LastWrite);

    /// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

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

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

    public override bool Execute()
    {
        if (!SemanticVersion.TryParse(SdkVersion, out var currentSdkVersion))
        {
            Log.LogError($"Invalid version: {SdkVersion}");
            return false;
        }

        var globalJsonPath = TaskEnvironment.GetAbsolutePath(Path.Combine(RepositoryRoot, "global.json"));
        DateTime lastWrite;
        try
        {
            lastWrite = File.GetLastWriteTimeUtc(globalJsonPath);
        }
        catch (Exception e)
        {
            Log.LogError($"Error accessing file '{globalJsonPath}': {e.Message}");
            return false;
        }

        // The read/write pair below is not atomic, so under multithreaded execution two threads
        // can both miss and both run the check. The check itself is pure, so the result is
        // identical either way; the only observable effect is that a failing check can log its
        // error twice, since deduplicating that reporting is part of what the cache buys.
        var cacheKey = new CacheKey(globalJsonPath, SdkVersion, lastWrite);
        if (BuildEngine4.GetRegisteredTaskObject(cacheKey, RegisteredTaskObjectLifetime.Build) is bool cachedSuccess)
        {
            // Error has already been reported if the current SDK version is not sufficient.
            if (!cachedSuccess)
            {
                Log.LogMessage(MessageImportance.Low, $"Previous .NET Core SDK version check failed.");
            }

            return cachedSuccess;
        }

        bool execute()
        {
            string globalJson;
            try
            {
                globalJson = File.ReadAllText(globalJsonPath);
            }
            catch (Exception e)
            {
                Log.LogError($"Error reading file '{globalJsonPath}': {e.Message}");
                return false;
            }

            // avoid Newtonsoft.Json dependency
            var match = Regex.Match(globalJson, $@"""dotnet""\s*:\s*""([^""]+)""");
            if (!match.Success)
            {
                Log.LogError($"Unable to determine dotnet version from file '{globalJsonPath}'.");
                return false;
            }

            var minSdkVersionStr = match.Groups[1].Value;
            if (!SemanticVersion.TryParse(minSdkVersionStr, out var minSdkVersion))
            {
                Log.LogError($"DotNet version specified in '{globalJsonPath}' is invalid: {minSdkVersionStr}.");
                return false;
            }

            if (currentSdkVersion < minSdkVersion)
            {
                Log.LogError($"The .NET Core SDK version {currentSdkVersion} is below the minimum required version {minSdkVersion}. You can install newer .NET Core SDK from https://www.microsoft.com/net/download.");
                return false;
            }

            return true;
        }

        bool success = execute();
        BuildEngine4.RegisterTaskObject(cacheKey, success, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: true);
        return success;
    }
}