File: ToolsetInfo.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.TestFramework\Microsoft.NET.TestFramework.csproj (Microsoft.NET.TestFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.DotNet.Cli.Utils;
 
namespace Microsoft.NET.TestFramework
{
    public class ToolsetInfo
    {
        public const string CurrentTargetFramework = "net10.0";
        public const string CurrentTargetFrameworkVersion = "10.0";
        public const string CurrentTargetFrameworkMoniker = ".NETCoreApp,Version=v" + CurrentTargetFrameworkVersion;
        public const string NextTargetFramework = "net11.0";
        public const string NextTargetFrameworkVersion = "11.0";
 
        public const string LatestWinRuntimeIdentifier = "win";
        public const string LatestLinuxRuntimeIdentifier = "linux";
        public const string LatestMacRuntimeIdentifier = "osx";
        public const string LatestRuntimeIdentifiers = $"{LatestWinRuntimeIdentifier}-x64;{LatestWinRuntimeIdentifier}-x86;{LatestMacRuntimeIdentifier}-x64;{LatestLinuxRuntimeIdentifier}-x64;linux-musl-x64";
 
        public string DotNetRoot { get; }
        public string DotNetHostPath { get; }
 
        private string? _sdkVersion;
        public string SdkVersion
        {
            get
            {
                if (_sdkVersion == null)
                {
                    //  Initialize SdkVersion lazily, as we call `dotnet --version` to get it, so we need to wait
                    //  for the TestContext to finish being initialize
                    InitSdkVersion();
                }
                return _sdkVersion ?? throw new InvalidOperationException("SdkVersion should never be null."); ;
            }
        }
 
        private string? _msbuildVersion;
        public string? MSBuildVersion
        {
            get
            {
                if (_msbuildVersion == null)
                {
                    //  Initialize MSBuildVersion lazily, as we call `dotnet msbuild -version` to get it, so we need to wait
                    //  for the TestContext to finish being initialize
                    InitMSBuildVersion();
                }
                return _msbuildVersion;
            }
        }
 
        Lazy<string> _sdkFolderUnderTest;
 
        public string SdkFolderUnderTest => _sdkFolderUnderTest.Value;
 
        Lazy<string> _sdksPath;
        public string SdksPath => _sdksPath.Value;
 
        public string? CliHomePath { get; set; }
 
        public string? MicrosoftNETBuildExtensionsPathOverride { get; set; }
 
        public bool ShouldUseFullFrameworkMSBuild => !string.IsNullOrEmpty(FullFrameworkMSBuildPath);
 
        public string? FullFrameworkMSBuildPath { get; set; }
 
        public string? SdkResolverPath { get; set; }
 
        public string? RepoRoot { get; set; }
 
        public ToolsetInfo(string dotNetRoot)
        {
            DotNetRoot = dotNetRoot;
 
            DotNetHostPath = Path.Combine(dotNetRoot, $"dotnet{Constants.ExeSuffix}");
 
            _sdkFolderUnderTest = new Lazy<string>(() => Path.Combine(DotNetRoot, "sdk", SdkVersion));
            _sdksPath = new Lazy<string>(() => Path.Combine(SdkFolderUnderTest, "Sdks"));
        }
 
        private void InitSdkVersion()
        {
            //  If using full framework MSBuild, then running a command tries to get the SdkVersion in order to set the
            //  DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR environment variable.  So turn that off when getting the SDK version
            //  in order to avoid stack overflow
            string? oldFullFrameworkMSBuildPath = FullFrameworkMSBuildPath;
            try
            {
                FullFrameworkMSBuildPath = null;
                var logger = new StringTestLogger();
                var command = new DotnetCommand(logger, "--version")
                {
                    WorkingDirectory = TestContext.Current.TestExecutionDirectory
                };
 
                var result = command.Execute();
 
                if (result.ExitCode != 0)
                {
                    throw new Exception("Failed to get dotnet version" + Environment.NewLine + logger.ToString());
                }
 
                _sdkVersion = result.StdOut?.Trim();
            }
            finally
            {
                FullFrameworkMSBuildPath = oldFullFrameworkMSBuildPath;
            }
        }
 
        private void InitMSBuildVersion()
        {
            var logger = new StringTestLogger();
            var command = new MSBuildVersionCommand(logger)
            {
                WorkingDirectory = TestContext.Current.TestExecutionDirectory
            };
 
            var result = command.Execute();
 
            if (result.ExitCode != 0)
            {
                throw new Exception("Failed to get msbuild version" + Environment.NewLine + logger.ToString());
            }
 
            _msbuildVersion = result.StdOut?.Split().Last();
        }
 
        public string? GetMicrosoftNETBuildExtensionsPath()
        {
            if (!string.IsNullOrEmpty(MicrosoftNETBuildExtensionsPathOverride))
            {
                return MicrosoftNETBuildExtensionsPathOverride;
            }
            else
            {
                if (ShouldUseFullFrameworkMSBuild)
                {
                    string? msbuildRoot = null;
                    var msbuildBinPath = Path.GetDirectoryName(FullFrameworkMSBuildPath);
                    if (msbuildBinPath is not null)
                    {
                        msbuildRoot = Directory.GetParent(msbuildBinPath)?.Parent?.FullName;
                    }
                    return Path.Combine(msbuildRoot ?? string.Empty, @"Microsoft\Microsoft.NET.Build.Extensions");
                }
                else
                {
                    return Path.Combine(DotNetRoot, "sdk", SdkVersion, @"Microsoft\Microsoft.NET.Build.Extensions");
                }
            }
        }
 
        public void AddTestEnvironmentVariables(IDictionary<string, string> environment)
        {
            if (ShouldUseFullFrameworkMSBuild)
            {
                string sdksPath = Path.Combine(DotNetRoot, "sdk", SdkVersion, "Sdks");
 
                //  Use stage 2 MSBuild SDK resolver
                if (SdkResolverPath is not null)
                {
                    environment["MSBUILDADDITIONALSDKRESOLVERSFOLDER_NETFRAMEWORK"] = SdkResolverPath;
                }
 
                //  Avoid using stage 0 dotnet install dir
                environment["DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"] = "";
 
                //  Put stage 2 on the Path (this is how the MSBuild SDK resolver finds dotnet)
                environment["Path"] = DotNetRoot + ";" + Environment.GetEnvironmentVariable("Path");
 
                if (!string.IsNullOrEmpty(MicrosoftNETBuildExtensionsPathOverride))
                {
                    var microsoftNETBuildExtensionsPath = GetMicrosoftNETBuildExtensionsPath();
                    if (microsoftNETBuildExtensionsPath is not null)
                    {
                        environment["MicrosoftNETBuildExtensionsTargets"] = Path.Combine(microsoftNETBuildExtensionsPath, "Microsoft.NET.Build.Extensions.targets");
                    }
 
                    if (UsingFullMSBuildWithoutExtensionsTargets())
                    {
                        environment["CustomAfterMicrosoftCommonTargets"] = Path.Combine(sdksPath, "Microsoft.NET.Build.Extensions",
                            "msbuildExtensions-ver", "Microsoft.Common.targets", "ImportAfter", "Microsoft.NET.Build.Extensions.targets");
                    }
                }
 
            }
 
            if (Environment.Is64BitProcess)
            {
                environment.Add("DOTNET_ROOT", DotNetRoot);
            }
            else
            {
                environment.Add("DOTNET_ROOT(x86)", DotNetRoot);
            }
 
            if (!string.IsNullOrEmpty(CliHomePath))
            {
                environment.Add("DOTNET_CLI_HOME", CliHomePath);
            }
 
            //  We set this environment variable for in-process tests, but we don't want it to flow to out of process tests
            //  (especially if we're trying to run on full Framework MSBuild)
            environment[Constants.MSBUILD_EXE_PATH] = "";
 
        }
 
        public SdkCommandSpec CreateCommandForTarget(string target, IEnumerable<string> args)
        {
            var newArgs = args.ToList();
            if (!string.IsNullOrEmpty(target))
            {
                newArgs.Insert(0, $"/t:{target}");
            }
 
            return CreateCommand(newArgs.ToArray());
        }
 
        private SdkCommandSpec CreateCommand(params string[] args)
        {
            SdkCommandSpec ret = new();
 
            //  Run tests on full framework MSBuild if environment variable is set pointing to it
            if (ShouldUseFullFrameworkMSBuild)
            {
                ret.FileName = FullFrameworkMSBuildPath;
                ret.Arguments = args.ToList();
                // Don't propagate DOTNET_HOST_PATH to the msbuild process, to match behavior
                // when running desktop msbuild outside of the test harness.
                ret.Environment["DOTNET_HOST_PATH"] = string.Empty;
            }
            else
            {
                var newArgs = args.ToList();
                newArgs.Insert(0, $"msbuild");
 
                ret.FileName = DotNetHostPath;
                ret.Arguments = newArgs;
            }
 
            TestContext.Current.AddTestEnvironmentVariables(ret.Environment);
 
            return ret;
        }
 
        private static string GetDotnetHostPath(string? dotnetRoot)
            => Path.Combine(dotnetRoot ?? string.Empty, "dotnet" + Constants.ExeSuffix);
 
        public static ToolsetInfo Create(string? repoRoot, string? repoArtifactsDir, string configuration, TestCommandLine commandLine)
        {
            repoRoot = commandLine.SDKRepoPath ?? repoRoot;
            configuration = commandLine.SDKRepoConfiguration ?? configuration;
 
            string? dotnetInstallDirFromEnvironment = Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR");
 
            string? dotnetRoot;
            string hostNotFoundReason;
 
            if (!string.IsNullOrEmpty(commandLine.DotnetHostPath))
            {
                dotnetRoot = Path.GetDirectoryName(commandLine.DotnetHostPath);
                hostNotFoundReason = "Command line argument -dotnetPath is incorrect.";
            }
            else if (repoRoot != null && repoArtifactsDir is not null)
            {
                dotnetRoot = Path.Combine(repoArtifactsDir, "bin", "redist", configuration, "dotnet");
                hostNotFoundReason = "Is 'redist.csproj' built?";
            }
            else if (!string.IsNullOrEmpty(dotnetInstallDirFromEnvironment))
            {
                dotnetRoot = dotnetInstallDirFromEnvironment;
                hostNotFoundReason = "The value of DOTNET_INSTALL_DIR is incorrect.";
            }
            else
            {
                if (TryResolveCommand("dotnet", out string? pathToDotnet))
                {
                    dotnetRoot = Path.GetDirectoryName(pathToDotnet);
                }
                else
                {
                    throw new InvalidOperationException("Could not resolve path to dotnet");
                }
                hostNotFoundReason = "";
            }
 
            var dotnetHost = GetDotnetHostPath(dotnetRoot);
            if (dotnetRoot is null || !File.Exists(dotnetHost))
            {
                throw new FileNotFoundException($"Host '{dotnetHost}' not found. {hostNotFoundReason}");
            }
 
            var ret = new ToolsetInfo(dotnetRoot)
            {
                RepoRoot = repoRoot,
            };
 
            if (!string.IsNullOrEmpty(commandLine.FullFrameworkMSBuildPath))
            {
                ret.FullFrameworkMSBuildPath = commandLine.FullFrameworkMSBuildPath;
            }
            else if (commandLine.UseFullFrameworkMSBuild)
            {
                if (TryResolveCommand("MSBuild", out string? pathToMSBuild))
                {
                    ret.FullFrameworkMSBuildPath = pathToMSBuild;
                }
                else
                {
                    throw new InvalidOperationException("Could not resolve path to MSBuild");
                }
            }
 
            var microsoftNETBuildExtensionsTargetsFromEnvironment = Environment.GetEnvironmentVariable("MicrosoftNETBuildExtensionsTargets");
            if (!string.IsNullOrWhiteSpace(microsoftNETBuildExtensionsTargetsFromEnvironment))
            {
                ret.MicrosoftNETBuildExtensionsPathOverride = Path.GetDirectoryName(microsoftNETBuildExtensionsTargetsFromEnvironment);
            }
            else if (repoRoot != null && ret.ShouldUseFullFrameworkMSBuild && repoArtifactsDir is not null)
            {
                //  Find path to Microsoft.NET.Build.Extensions for full framework
                string sdksPath = Path.Combine(repoArtifactsDir, "bin", configuration, "Sdks");
                var buildExtensionsSdkPath = Path.Combine(sdksPath, "Microsoft.NET.Build.Extensions");
                ret.MicrosoftNETBuildExtensionsPathOverride = Path.Combine(buildExtensionsSdkPath, "msbuildExtensions", "Microsoft", "Microsoft.NET.Build.Extensions");
            }
 
            if (ret.ShouldUseFullFrameworkMSBuild)
            {
                if (repoRoot != null && repoArtifactsDir is not null)
                {
                    // Find path to MSBuildSdkResolver for full framework
                    ret.SdkResolverPath = Path.Combine(repoArtifactsDir, "bin", "Microsoft.DotNet.MSBuildSdkResolver", configuration, "net472", "SdkResolvers");
                }
                else if (!string.IsNullOrWhiteSpace(commandLine.MsbuildAdditionalSdkResolverFolder))
                {
                    ret.SdkResolverPath = Path.Combine(commandLine.MsbuildAdditionalSdkResolverFolder, configuration, "net472", "SdkResolvers");
                }
                else if (Environment.GetEnvironmentVariable("DOTNET_SDK_TEST_MSBUILDSDKRESOLVER_FOLDER") != null)
                {
                    ret.SdkResolverPath = Path.Combine(Environment.GetEnvironmentVariable("DOTNET_SDK_TEST_MSBUILDSDKRESOLVER_FOLDER")!, configuration, "net472", "SdkResolvers");
                }
                else
                {
                    throw new InvalidOperationException("Microsoft.DotNet.MSBuildSdkResolver path is not provided, set msbuildAdditionalSdkResolverFolder on test commandline or set repoRoot");
                }
            }
 
            if (repoRoot != null && repoArtifactsDir is not null)
            {
                ret.CliHomePath = Path.Combine(repoArtifactsDir, "tmp", configuration, "testing");
            }
 
            return ret;
        }
 
        /// <summary>
        /// Attempts to resolve full path to command from PATH/PATHEXT environment variable.
        /// </summary>
        /// <param name="command">The command to resolve.</param>
        /// <param name="fullExePath">The full path to the command</param>
        /// <returns><see langword="true"/> when command can be resolved, <see langword="false"/> otherwise.</returns>
        public static bool TryResolveCommand(string command, out string? fullExePath)
        {
            fullExePath = null;
            char pathSplitChar;
            string[] extensions = new string[] { string.Empty };
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                pathSplitChar = ';';
                extensions = extensions
                    .Concat(Environment.GetEnvironmentVariable("PATHEXT")?.Split(pathSplitChar) ?? Array.Empty<string>())
                    .ToArray();
            }
            else
            {
                pathSplitChar = ':';
            }
 
            var paths = Environment.GetEnvironmentVariable("PATH")?.Split(pathSplitChar);
            string? result = extensions.SelectMany(ext => paths?.Select(p => Path.Combine(p, command + ext)) ?? Array.Empty<string>())
                .FirstOrDefault(File.Exists);
 
            if (result == null)
            {
                return false;
            }
 
            fullExePath = result;
            return true;
        }
 
        private bool UsingFullMSBuildWithoutExtensionsTargets()
        {
            if (!ShouldUseFullFrameworkMSBuild)
            {
                return false;
            }
            string? fullMSBuildDirectory = Path.GetDirectoryName(FullFrameworkMSBuildPath);
            string extensionsImportAfterPath = Path.Combine(fullMSBuildDirectory ?? string.Empty, "..", "Microsoft.Common.targets", "ImportAfter", "Microsoft.NET.Build.Extensions.targets");
            return !File.Exists(extensionsImportAfterPath);
        }
 
        internal static IEnumerable<(string versionPropertyName, string version)> GetPackageVersionProperties()
            => typeof(ToolsetInfo).Assembly
                .GetCustomAttributes<AssemblyMetadataAttribute>()
                .Where(a => a.Key is not null && a.Key.EndsWith("PackageVersion"))
                .Select(a => (a.Key ?? string.Empty, a.Value ?? string.Empty));
 
        public static string GetPackageVersion(string packageName)
        {
            var propertyName = packageName.Replace(".", "") + "PackageVersion";
            return GetPackageVersionProperties().Single(p => p.versionPropertyName == propertyName).version;
        }
 
        private static readonly Lazy<string> s_newtonsoftJsonPackageVersion = new(() => GetPackageVersion("Newtonsoft.Json"));
        private static readonly Lazy<string> s_systemDataSqlClientPackageVersion = new(() => GetPackageVersion("System.Data.SqlClient"));
 
        public static string GetNewtonsoftJsonPackageVersion()
            =>s_newtonsoftJsonPackageVersion.Value;
 
        public static string GetSystemDataSqlClientPackageVersion()
            => s_systemDataSqlClientPackageVersion.Value;
    }
}