| 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; } }