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

using System;
#if NET
using System.Diagnostics.CodeAnalysis;
#endif
using System.IO;

namespace Microsoft.DotNet.Cli.Utils;

/// <summary>
///  Canonical SDK filesystem locations for both the managed CLI and the Native AOT CLI. The first is
///  <see cref="SdkDirectory"/> - the versioned SDK directory (the folder that contains <c>dotnet.dll</c>,
///  <c>MSBuild.dll</c>, <c>Sdks\</c>, <c>DotnetTools\</c>, and the SDK targets). Prefer these over BCL
///  "where am I" APIs so paths resolve correctly in both hosts.
/// </summary>
/// <remarks>
///  <para>
///   Under the Native AOT CLI (<c>dotnet-aot</c> loaded directly by the muxer) the BCL location APIs do
///   NOT point at the versioned SDK directory: <see cref="System.AppContext.BaseDirectory"/> and
///   <c>Environment.ProcessPath</c> resolve to the install root (the muxer's own
///   directory) and <c>Assembly.Location</c> is empty. The AOT entry point therefore publishes the
///   resolved SDK directory as the <c>Microsoft.DotNet.Sdk.Root</c> AppContext value, which this helper
///   reads first. Code that needs an SDK-relative path (MSBuild, tasks, tools, forwarders) should use
///   this helper rather than probing a dll path.
///  </para>
///  <para>
///   Resolution order (resolved once and cached): the <c>Microsoft.DotNet.Sdk.Root</c> AppContext value,
///   then the directory of the SDK assembly (empty under single-file / NativeAOT), then
///   <see cref="System.AppContext.BaseDirectory"/>. See <c>src/Cli/dotnet-aot/SdkRootResolution.md</c>.
///  </para>
/// </remarks>
internal static class SdkPaths
{
    /// <summary>
    ///  The <see cref="System.AppContext"/> data name the Native AOT bridge uses to publish the resolved
    ///  versioned SDK directory for the assemblies compiled into it. An AppContext value is process-local
    ///  (unlike an environment variable it is not inherited by child processes) and can also be supplied
    ///  through a <c>runtimeconfig.json</c> <c>configProperties</c> entry.
    /// </summary>
    public const string DataName = "Microsoft.DotNet.Sdk.Root";

    private static string? s_sdkDirectory;

    /// <summary>
    ///  The versioned SDK directory, resolved once and cached. Prefers the <c>Microsoft.DotNet.Sdk.Root</c>
    ///  AppContext value (published by the AOT bridge); otherwise the directory of the SDK assembly, else
    ///  <see cref="System.AppContext.BaseDirectory"/>.
    /// </summary>
    public static string SdkDirectory => s_sdkDirectory ??= ResolveSdkDirectory();

#if NET
    [UnconditionalSuppressMessage("AOT", "IL3000",
        Justification = "Assembly.Location is empty under single-file / NativeAOT; the empty result is " +
            "handled by falling back to AppContext.BaseDirectory, and the AOT bridge sets the " +
            "Microsoft.DotNet.Sdk.Root AppContext value (preferred above), so this path is only reached " +
            "by the JIT-compiled managed CLI where Assembly.Location is the versioned SDK directory.")]
#endif
    internal static string ResolveSdkDirectory()
    {
        // The AOT bridge publishes the resolved SDK directory as the Microsoft.DotNet.Sdk.Root AppContext
        // value; prefer it.
        if (AppContext.GetData(DataName) is string sdkRoot && sdkRoot.Length > 0)
        {
            return sdkRoot;
        }

        // CoreUtils ships in the versioned SDK directory, so the location of this assembly is that
        // directory for the JIT-compiled managed CLI. Under a single-file / NativeAOT deployment
        // Assembly.Location is empty (which is what the IL3000 analyzer flags); fall through to
        // AppContext.BaseDirectory in that case - the AOT bridge sets the AppContext value (preferred above).
        string? assemblyDirectory = Path.GetDirectoryName(typeof(SdkPaths).Assembly.Location);
        return string.IsNullOrEmpty(assemblyDirectory) ? AppContext.BaseDirectory : assemblyDirectory;
    }
}