| File: src\LocateDotNet.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; namespace Microsoft.DotNet.Arcade.Sdk; [MSBuildMultiThreadableTask] public class LocateDotNet : Task, IMultiThreadableTask { private readonly record struct CacheKey(AbsolutePath GlobalJsonPath, DateTime LastWrite, string Paths); /// <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; } [Output] public string DotNetPath { get; set; } public override bool Execute() { ExecuteImpl(); return !Log.HasLoggedErrors; } private void ExecuteImpl() { var globalJsonPath = TaskEnvironment.GetAbsolutePath(Path.Combine(RepositoryRoot, "global.json")); var lastWrite = File.GetLastWriteTimeUtc(globalJsonPath); var paths = TaskEnvironment.GetEnvironmentVariable("PATH"); // GetEnvironmentVariable is nullable, and under multithreaded execution it is a lookup in a // per-project environment rather than the process block, so a missing PATH is more reachable // than it used to be. Fail with a clear message instead of a NullReferenceException below. if (string.IsNullOrEmpty(paths)) { Log.LogError("Unable to locate dotnet because the PATH environment variable is not set."); return; } // The read/write pair below is not atomic, so under multithreaded execution two threads // can both miss and both compute the value. That is benign here: the computation is pure // and deterministic for a given (global.json, timestamp, PATH), so the loser of the race // has its registration dropped and the winner's identical entry stands. The cache is an // optimization, not a lock. var cacheKey = new CacheKey(globalJsonPath, lastWrite, paths); if (BuildEngine4.GetRegisteredTaskObject(cacheKey, RegisteredTaskObjectLifetime.Build) is string cachedPath) { Log.LogMessage(MessageImportance.Low, $"Reused cached value."); DotNetPath = cachedPath; return; } var globalJson = File.ReadAllText(globalJsonPath); // 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; } var sdkVersion = match.Groups[1].Value; var fileName = (Path.DirectorySeparatorChar == '\\') ? "dotnet.exe" : "dotnet"; // Split with RemoveEmptyEntries, so no entry is empty and GetAbsolutePath cannot reject one. var dotNetDir = paths.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(p => File.Exists(TaskEnvironment.GetAbsolutePath(Path.Combine(p, fileName)))); if (dotNetDir == null || !Directory.Exists(TaskEnvironment.GetAbsolutePath(Path.Combine(dotNetDir, "sdk", sdkVersion)))) { Log.LogError($"Unable to find dotnet with SDK version '{sdkVersion}'"); return; } // GetAbsolutePath absolutizes but does not canonicalize, so canonicalize explicitly to keep // this [Output] in the same form the previous Path.GetFullPath produced. PATH entries // routinely carry '..' segments and trailing separators. DotNetPath = TaskEnvironment.GetAbsolutePath(Path.Combine(dotNetDir, fileName)).GetCanonicalForm(); BuildEngine4.RegisterTaskObject(cacheKey, DotNetPath, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: true); } }