File: BuildEnvironmentHelper.cs
Web Access
Project: ..\..\..\src\MSBuildTaskHost\MSBuildTaskHost.csproj (MSBuildTaskHost)
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared.FileSystem;
 
#nullable disable
 
namespace Microsoft.Build.Shared
{
    internal sealed class BuildEnvironmentHelper
    {
        // Since this class is added as 'link' to shared source in multiple projects,
        // MSBuildConstants.CurrentVisualStudioVersion is not available in all of them.
        private const string CurrentVisualStudioVersion = "17.0";
 
        // MSBuildConstants.CurrentToolsVersion
        private const string CurrentToolsVersion = "Current";
 
        /// <summary>
        /// Name of the Visual Studio (and Blend) process.
        /// VS ASP intellisense server fails without Microsoft.VisualStudio.Web.Host. Remove when issue fixed: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/574986
        /// </summary>
        private static readonly string[] s_visualStudioProcess = { "DEVENV", "BLEND", "Microsoft.VisualStudio.Web.Host" };
 
        /// <summary>
        /// Name of the MSBuild process(es)
        /// </summary>
        private static readonly string[] s_msBuildProcess = { "MSBUILD", "MSBUILDTASKHOST" };
 
        /// <summary>
        /// Name of MSBuild executable files.
        /// </summary>
        private static readonly string[] s_msBuildExeNames = { "MSBuild.exe", "MSBuild.dll" };
 
        /// <summary>
        /// Gets the cached Build Environment instance.
        /// </summary>
        public static BuildEnvironment Instance
        {
            get
            {
                try
                {
                    return BuildEnvironmentHelperSingleton.s_instance;
                }
                catch (TypeInitializationException e)
                {
                    if (e.InnerException != null)
                    {
                        // Throw the error that caused the TypeInitializationException.
                        // (likely InvalidOperationException)
                        throw e.InnerException;
                    }
 
                    throw;
                }
            }
        }
 
        /// <summary>
        /// Find the location of MSBuild.exe based on the current environment.
        /// </summary>
        /// <remarks>
        /// This defines the order and precedence for various methods of discovering MSBuild and associated toolsets.
        /// At a high level, an install under Visual Studio is preferred as the user may have SDKs installed to a
        /// specific instance of Visual Studio and build will only succeed if we can discover those. See
        /// https://github.com/dotnet/msbuild/issues/1461 for details.
        /// </remarks>
        /// <returns>Build environment.</returns>
        private static BuildEnvironment Initialize()
        {
            // See https://github.com/dotnet/msbuild/issues/1461 for specification of ordering and details.
            var possibleLocations = new Func<BuildEnvironment>[]
            {
                TryFromEnvironmentVariable,
                TryFromVisualStudioProcess,
                TryFromMSBuildProcess,
                TryFromMSBuildAssembly,
                TryFromDevConsole,
                TryFromSetupApi,
                TryFromAppContextBaseDirectory
            };
 
            foreach (var location in possibleLocations)
            {
                var env = location();
                if (env != null)
                {
                    return env;
                }
            }
 
            // If we can't find a suitable environment, continue in the 'None' mode. If not running tests,
            // we will use the current running process for the CurrentMSBuildExePath value.  This is likely
            // wrong, but many things use the CurrentMSBuildToolsDirectory value which must be set for basic
            // functionality to work.
            //
            // If we are running tests, then the current running process may be a test runner located in the
            // NuGet packages folder.  So in that case, we use the location of the current assembly, which
            // will be in the output path of the test project, which is what we want.
 
            string msbuildExePath;
            if (s_runningTests())
            {
                msbuildExePath = typeof(BuildEnvironmentHelper).Assembly.Location;
            }
            else
            {
                msbuildExePath = s_getProcessFromRunningProcess();
            }
 
            return new BuildEnvironment(
                BuildEnvironmentMode.None,
                msbuildExePath,
                runningTests: s_runningTests(),
                runningInMSBuildExe: false,
                runningInVisualStudio: false,
                visualStudioPath: null);
        }
 
        private static BuildEnvironment TryFromEnvironmentVariable()
        {
            var msBuildExePath = s_getEnvironmentVariable("MSBUILD_EXE_PATH");
 
            return msBuildExePath == null
                ? null
                : TryFromMSBuildExeUnderVisualStudio(msBuildExePath, allowLegacyToolsVersion: true) ?? TryFromStandaloneMSBuildExe(msBuildExePath);
        }
 
        private static BuildEnvironment TryFromVisualStudioProcess()
        {
            if (!NativeMethodsShared.IsWindows)
            {
                return null;
            }
 
            var vsProcess = s_getProcessFromRunningProcess();
            if (!IsProcessInList(vsProcess, s_visualStudioProcess))
            {
                return null;
            }
 
            var vsRoot = FileUtilities.GetFolderAbove(vsProcess, 3);
            string msBuildExe = GetMSBuildExeFromVsRoot(vsRoot);
 
            return new BuildEnvironment(
                BuildEnvironmentMode.VisualStudio,
                msBuildExe,
                runningTests: false,
                runningInMSBuildExe: false,
                runningInVisualStudio: true,
                visualStudioPath: vsRoot);
        }
 
        private static BuildEnvironment TryFromMSBuildProcess()
        {
            var msBuildExe = s_getProcessFromRunningProcess();
            if (!IsProcessInList(msBuildExe, s_msBuildProcess))
            {
                return null;
            }
 
            // First check if we're in a VS installation
            if (NativeMethodsShared.IsWindows &&
                Regex.IsMatch(msBuildExe, $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*MSBuild(?:TaskHost)?\.exe", RegexOptions.IgnoreCase))
            {
                return new BuildEnvironment(
                    BuildEnvironmentMode.VisualStudio,
                    msBuildExe,
                    runningTests: false,
                    runningInMSBuildExe: true,
                    runningInVisualStudio: false,
                    visualStudioPath: GetVsRootFromMSBuildAssembly(msBuildExe));
            }
 
            // Standalone mode running in MSBuild.exe
            return new BuildEnvironment(
                BuildEnvironmentMode.Standalone,
                msBuildExe,
                runningTests: false,
                runningInMSBuildExe: true,
                runningInVisualStudio: false,
                visualStudioPath: null);
        }
 
        private static BuildEnvironment TryFromMSBuildAssembly()
        {
            var buildAssembly = s_getExecutingAssemblyPath();
            if (buildAssembly == null)
            {
                return null;
            }
 
            // Check for MSBuild.[exe|dll] next to the current assembly
            var msBuildExe = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.exe");
            var msBuildDll = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.dll");
 
            // First check if we're in a VS installation
            var environment = TryFromMSBuildExeUnderVisualStudio(msBuildExe);
            if (environment != null)
            {
                return environment;
            }
 
            // We're not in VS, check for MSBuild.exe / dll to consider this a standalone environment.
            string msBuildPath = null;
            if (FileSystems.Default.FileExists(msBuildExe))
            {
                msBuildPath = msBuildExe;
            }
            else if (FileSystems.Default.FileExists(msBuildDll))
            {
                msBuildPath = msBuildDll;
            }
 
            if (!string.IsNullOrEmpty(msBuildPath))
            {
                // Standalone mode with toolset
                return new BuildEnvironment(
                    BuildEnvironmentMode.Standalone,
                    msBuildPath,
                    runningTests: s_runningTests(),
                    runningInMSBuildExe: false,
                    runningInVisualStudio: false,
                    visualStudioPath: null);
            }
 
            return null;
        }
 
        private static BuildEnvironment TryFromMSBuildExeUnderVisualStudio(string msbuildExe, bool allowLegacyToolsVersion = false)
        {
            string msBuildPathPattern = allowLegacyToolsVersion
                ? $@".*\\MSBuild\\({CurrentToolsVersion}|\d+\.0)\\Bin\\.*"
                : $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*";
 
            if (NativeMethodsShared.IsWindows &&
                Regex.IsMatch(msbuildExe, msBuildPathPattern, RegexOptions.IgnoreCase))
            {
                string visualStudioRoot = GetVsRootFromMSBuildAssembly(msbuildExe);
                return new BuildEnvironment(
                        BuildEnvironmentMode.VisualStudio,
                        GetMSBuildExeFromVsRoot(visualStudioRoot),
                        runningTests: s_runningTests(),
                        runningInMSBuildExe: false,
                        runningInVisualStudio: false,
                        visualStudioPath: visualStudioRoot);
            }
 
            return null;
        }
 
        private static BuildEnvironment TryFromDevConsole()
        {
            if (s_runningTests())
            {
                // If running unit tests, then don't try to get the build environment from MSBuild installed on the machine
                //  (we should be using the locally built MSBuild instead)
                return null;
            }
 
            // VSINSTALLDIR and VisualStudioVersion are set from the Developer Command Prompt.
            var vsInstallDir = s_getEnvironmentVariable("VSINSTALLDIR");
            var vsVersion = s_getEnvironmentVariable("VisualStudioVersion");
 
            if (string.IsNullOrEmpty(vsInstallDir) || string.IsNullOrEmpty(vsVersion) ||
                vsVersion != CurrentVisualStudioVersion || !FileSystems.Default.DirectoryExists(vsInstallDir))
            {
                return null;
            }
 
            return new BuildEnvironment(
                BuildEnvironmentMode.VisualStudio,
                GetMSBuildExeFromVsRoot(vsInstallDir),
                runningTests: false,
                runningInMSBuildExe: false,
                runningInVisualStudio: false,
                visualStudioPath: vsInstallDir);
        }
 
        private static BuildEnvironment TryFromSetupApi()
        {
            if (s_runningTests())
            {
                // If running unit tests, then don't try to get the build environment from MSBuild installed on the machine
                //  (we should be using the locally built MSBuild instead)
                return null;
            }
 
            Version v = new Version(CurrentVisualStudioVersion);
            var instances = s_getVisualStudioInstances()
                .Where(i => i.Version.Major == v.Major && FileSystems.Default.DirectoryExists(i.Path))
                .ToList();
 
            if (instances.Count == 0)
            {
                return null;
            }
 
            if (instances.Count > 1)
            {
                // TODO: Warn user somehow. We may have picked the wrong one.
            }
 
            return new BuildEnvironment(
                BuildEnvironmentMode.VisualStudio,
                GetMSBuildExeFromVsRoot(instances[0].Path),
                runningTests: false,
                runningInMSBuildExe: false,
                runningInVisualStudio: false,
                visualStudioPath: instances[0].Path);
        }
 
        private static BuildEnvironment TryFromAppContextBaseDirectory()
        {
            // Assemblies compiled against anything older than .NET 4.0 won't have a System.AppContext
            // Try the base directory that the assembly resolver uses to probe for assemblies.
            // Under certain scenarios the assemblies are loaded from spurious locations like the NuGet package cache
            // but the toolset files are copied to the app's directory via "contentFiles".
 
            var appContextBaseDirectory = s_getAppContextBaseDirectory();
            if (string.IsNullOrEmpty(appContextBaseDirectory))
            {
                return null;
            }
 
            // Look for possible MSBuild exe names in the AppContextBaseDirectory
            return s_msBuildExeNames
                .Select((name) => TryFromStandaloneMSBuildExe(Path.Combine(appContextBaseDirectory, name)))
                .FirstOrDefault(env => env != null);
        }
 
        private static BuildEnvironment TryFromStandaloneMSBuildExe(string msBuildExePath)
        {
            if (!string.IsNullOrEmpty(msBuildExePath) && FileSystems.Default.FileExists(msBuildExePath))
            {
                // MSBuild.exe was found outside of Visual Studio. Assume Standalone mode.
                return new BuildEnvironment(
                    BuildEnvironmentMode.Standalone,
                    msBuildExePath,
                    runningTests: s_runningTests(),
                    runningInMSBuildExe: false,
                    runningInVisualStudio: false,
                    visualStudioPath: null);
            }
 
            return null;
        }
 
        private static string GetVsRootFromMSBuildAssembly(string msBuildAssembly)
        {
            string directory = Path.GetDirectoryName(msBuildAssembly);
            return FileUtilities.GetFolderAbove(msBuildAssembly,
                directory.EndsWith(@"\amd64", StringComparison.OrdinalIgnoreCase) ||
                directory.EndsWith(@"\arm64", StringComparison.OrdinalIgnoreCase)
                    ? 5
                    : 4);
        }
 
        private static string GetMSBuildExeFromVsRoot(string visualStudioRoot)
        {
            return FileUtilities.CombinePaths(
                visualStudioRoot,
                "MSBuild",
                CurrentToolsVersion,
                "Bin",
                NativeMethodsShared.ProcessorArchitecture == Framework.NativeMethods.ProcessorArchitectures.X64 ? "amd64" :
                NativeMethodsShared.ProcessorArchitecture == Framework.NativeMethods.ProcessorArchitectures.ARM64 ? "arm64" : string.Empty,
                "MSBuild.exe");
        }
 
        private static bool? _runningTests;
        private static readonly object _runningTestsLock = new object();
 
        private static bool CheckIfRunningTests()
        {
            if (_runningTests != null)
            {
                return _runningTests.Value;
            }
 
            lock (_runningTestsLock)
            {
                if (_runningTests != null)
                {
                    return _runningTests.Value;
                }
 
                // Check if running tests via the TestInfo class in Microsoft.Build.Framework.
                //  See the comments on the TestInfo class for an explanation of why it works this way.
                var frameworkAssembly = typeof(Framework.ITask).Assembly;
                var testInfoType = frameworkAssembly.GetType("Microsoft.Build.Framework.TestInfo");
                var runningTestsField = testInfoType.GetField("s_runningTests", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
 
                _runningTests = (bool)runningTestsField.GetValue(null);
 
                return _runningTests.Value;
            }
        }
 
        /// <summary>
        /// Returns true if processName appears in the processList
        /// </summary>
        /// <param name="processName">Name of the process</param>
        /// <param name="processList">List of processes to check</param>
        /// <returns></returns>
        private static bool IsProcessInList(string processName, string[] processList)
        {
            var processFileName = Path.GetFileNameWithoutExtension(processName);
 
            if (string.IsNullOrEmpty(processFileName))
            {
                return false;
            }
 
            return processList.Any(s =>
                processFileName.Equals(s, StringComparison.OrdinalIgnoreCase));
        }
 
        private static string GetProcessFromRunningProcess()
        {
#if RUNTIME_TYPE_NETCORE
            // The EntryAssembly property can return null when a managed assembly has been loaded from
            // an unmanaged application (for example, using custom CLR hosting).
            if (AssemblyUtilities.EntryAssembly == null)
            {
                return Process.GetCurrentProcess().MainModule.FileName;
            }
 
            return AssemblyUtilities.GetAssemblyLocation(AssemblyUtilities.EntryAssembly);
#else
            return Process.GetCurrentProcess().MainModule.FileName;
#endif
        }
 
        private static string GetExecutingAssemblyPath()
        {
            return FileUtilities.ExecutingAssemblyPath;
        }
 
        private static string GetAppContextBaseDirectory()
        {
#if !CLR2COMPATIBILITY // Assemblies compiled against anything older than .NET 4.0 won't have a System.AppContext
            return AppContext.BaseDirectory;
#else
            return null;
#endif
        }
 
        private static string GetEnvironmentVariable(string variable)
        {
            return Environment.GetEnvironmentVariable(variable);
        }
 
        /// <summary>
        /// Resets the current singleton instance (for testing).
        /// </summary>
        internal static void ResetInstance_ForUnitTestsOnly(Func<string> getProcessFromRunningProcess = null,
            Func<string> getExecutingAssemblyPath = null, Func<string> getAppContextBaseDirectory = null,
            Func<IEnumerable<VisualStudioInstance>> getVisualStudioInstances = null,
            Func<string, string> getEnvironmentVariable = null,
            Func<bool> runningTests = null)
        {
            s_getProcessFromRunningProcess = getProcessFromRunningProcess ?? GetProcessFromRunningProcess;
            s_getExecutingAssemblyPath = getExecutingAssemblyPath ?? GetExecutingAssemblyPath;
            s_getAppContextBaseDirectory = getAppContextBaseDirectory ?? GetAppContextBaseDirectory;
            s_getVisualStudioInstances = getVisualStudioInstances ?? VisualStudioLocationHelper.GetInstances;
            s_getEnvironmentVariable = getEnvironmentVariable ?? GetEnvironmentVariable;
 
            // Tests which specifically test the BuildEnvironmentHelper need it to be able to act as if it is not running tests
            s_runningTests = runningTests ?? CheckIfRunningTests;
 
            BuildEnvironmentHelperSingleton.s_instance = Initialize();
        }
 
        /// <summary>
        /// Resets the current singleton instance (for testing).
        /// </summary>
        internal static void ResetInstance_ForUnitTestsOnly(BuildEnvironment buildEnvironment)
        {
            BuildEnvironmentHelperSingleton.s_instance = buildEnvironment;
        }
 
        private static Func<string> s_getProcessFromRunningProcess = GetProcessFromRunningProcess;
        private static Func<string> s_getExecutingAssemblyPath = GetExecutingAssemblyPath;
        private static Func<string> s_getAppContextBaseDirectory = GetAppContextBaseDirectory;
        private static Func<IEnumerable<VisualStudioInstance>> s_getVisualStudioInstances = VisualStudioLocationHelper.GetInstances;
        private static Func<string, string> s_getEnvironmentVariable = GetEnvironmentVariable;
        private static Func<bool> s_runningTests = CheckIfRunningTests;
 
        private static class BuildEnvironmentHelperSingleton
        {
            // Explicit static constructor to tell C# compiler
            // not to mark type as beforefieldinit
            static BuildEnvironmentHelperSingleton()
            { }
 
            public static BuildEnvironment s_instance = Initialize();
        }
    }
 
    /// <summary>
    /// Enum which defines which environment / mode MSBuild is currently running.
    /// </summary>
    internal enum BuildEnvironmentMode
    {
        /// <summary>
        /// Running from Visual Studio directly or from MSBuild installed under an instance of Visual Studio.
        /// Toolsets and extensions will be loaded from the Visual Studio instance.
        /// </summary>
        VisualStudio,
 
        /// <summary>
        /// Running in a standalone toolset mode. All toolsets and extensions paths are relative to the app
        /// running and not dependent on Visual Studio. (e.g. dotnet CLI, open source clone of our repo)
        /// </summary>
        Standalone,
 
        /// <summary>
        /// Running without any defined toolsets. Most functionality limited. Likely will not be able to
        /// build or evaluate a project. (e.g. reference to Microsoft.*.dll without a toolset definition
        /// or Visual Studio instance installed).
        /// </summary>
        None
    }
 
    /// <summary>
    /// Defines the current environment for build tools.
    /// </summary>
    internal sealed class BuildEnvironment
    {
        public BuildEnvironment(BuildEnvironmentMode mode, string currentMSBuildExePath, bool runningTests, bool runningInMSBuildExe, bool runningInVisualStudio,
                string visualStudioPath)
        {
            FileInfo currentMSBuildExeFile = null;
            DirectoryInfo currentToolsDirectory = null;
 
            Mode = mode;
            RunningTests = runningTests;
            RunningInMSBuildExe = runningInMSBuildExe;
            RunningInVisualStudio = runningInVisualStudio;
            CurrentMSBuildExePath = currentMSBuildExePath;
            VisualStudioInstallRootDirectory = visualStudioPath;
 
#if !NO_FRAMEWORK_IVT
            Framework.BuildEnvironmentState.s_runningTests = runningTests;
            Framework.BuildEnvironmentState.s_runningInVisualStudio = runningInVisualStudio;
#endif
 
            if (!string.IsNullOrEmpty(currentMSBuildExePath))
            {
                currentMSBuildExeFile = new FileInfo(currentMSBuildExePath);
                currentToolsDirectory = currentMSBuildExeFile.Directory;
 
                CurrentMSBuildToolsDirectory = currentMSBuildExeFile.DirectoryName;
                CurrentMSBuildConfigurationFile = string.Concat(currentMSBuildExePath, ".config");
                MSBuildToolsDirectory32 = CurrentMSBuildToolsDirectory;
                MSBuildToolsDirectory64 = CurrentMSBuildToolsDirectory;
                MSBuildToolsDirectoryRoot = CurrentMSBuildToolsDirectory;
            }
 
            // We can't detect an environment, don't try to set other paths.
            if (mode == BuildEnvironmentMode.None || currentMSBuildExeFile == null || currentToolsDirectory == null)
            {
                return;
            }
 
            var msBuildExeName = currentMSBuildExeFile.Name;
 
            if (mode == BuildEnvironmentMode.VisualStudio)
            {
                // In Visual Studio, the entry-point MSBuild.exe is often from an arch-specific subfolder
                MSBuildToolsDirectoryRoot = NativeMethodsShared.ProcessorArchitecture switch
                {
                    NativeMethodsShared.ProcessorArchitectures.X86 => CurrentMSBuildToolsDirectory,
                    NativeMethodsShared.ProcessorArchitectures.X64 or NativeMethodsShared.ProcessorArchitectures.ARM64
                        => currentToolsDirectory.Parent?.FullName,
                    _ => throw new InternalErrorException("Unknown processor architecture " + NativeMethodsShared.ProcessorArchitecture),
                };
            }
            else
            {
                // In the .NET SDK, there's one copy of MSBuild.dll and it's in the root folder.
                MSBuildToolsDirectoryRoot = CurrentMSBuildToolsDirectory;
 
                // If we're standalone, we might not be in the SDK. Rely on folder paths at this point.
                if (string.Equals(currentToolsDirectory.Name, "amd64", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(currentToolsDirectory.Name, "arm64", StringComparison.OrdinalIgnoreCase))
                {
                    MSBuildToolsDirectoryRoot = currentToolsDirectory.Parent?.FullName;
                }
            }
 
            if (MSBuildToolsDirectoryRoot != null)
            {
                // Calculate potential paths to other architecture MSBuild.exe
                var potentialAmd64FromX86 = FileUtilities.CombinePaths(MSBuildToolsDirectoryRoot, "amd64", msBuildExeName);
                var potentialARM64FromX86 = FileUtilities.CombinePaths(MSBuildToolsDirectoryRoot, "arm64", msBuildExeName);
 
                // Check for existence of an MSBuild file. Note this is not necessary in a VS installation where we always want to
                // assume the correct layout.
                var existsCheck = mode == BuildEnvironmentMode.VisualStudio ? new Func<string, bool>(_ => true) : File.Exists;
 
                MSBuildToolsDirectory32 = MSBuildToolsDirectoryRoot;
                MSBuildToolsDirectory64 = existsCheck(potentialAmd64FromX86) ? Path.Combine(MSBuildToolsDirectoryRoot, "amd64") : CurrentMSBuildToolsDirectory;
#if RUNTIME_TYPE_NETCORE
                // Fall back to "current" for any architecture since .NET SDK doesn't
                // support cross-arch task invocations.
                MSBuildToolsDirectoryArm64 = existsCheck(potentialARM64FromX86) ? Path.Combine(MSBuildToolsDirectoryRoot, "arm64") : CurrentMSBuildToolsDirectory;
#else
                MSBuildToolsDirectoryArm64 = existsCheck(potentialARM64FromX86) ? Path.Combine(MSBuildToolsDirectoryRoot, "arm64") : null;
#endif
            }
 
            MSBuildExtensionsPath = mode == BuildEnvironmentMode.VisualStudio
                ? Path.Combine(VisualStudioInstallRootDirectory, "MSBuild")
                : MSBuildToolsDirectory32;
        }
 
        internal BuildEnvironmentMode Mode { get; }
 
        /// <summary>
        /// Gets the flag that indicates if we are running in a test harness.
        /// </summary>
        internal bool RunningTests { get; }
 
        /// <summary>
        /// Returns true when the entry point application is MSBuild.exe.
        /// </summary>
        internal bool RunningInMSBuildExe { get; }
 
        /// <summary>
        /// Returns true when the entry point application is Visual Studio.
        /// </summary>
        internal bool RunningInVisualStudio { get; }
 
        /// <summary>
        /// Path to the root of the MSBuild folder (in VS scenarios, <c>MSBuild\Current\bin</c>).
        /// </summary>
        internal string MSBuildToolsDirectoryRoot { get; }
 
        /// <summary>
        /// Path to the MSBuild 32-bit tools directory.
        /// </summary>
        internal string MSBuildToolsDirectory32 { get; }
 
        /// <summary>
        /// Path to the MSBuild 64-bit (AMD64) tools directory.
        /// </summary>
        internal string MSBuildToolsDirectory64 { get; }
 
        /// <summary>
        /// Path to the ARM64 tools directory.
        /// <see langword="null" /> if ARM64 tools are not installed.
        /// </summary>
        internal string MSBuildToolsDirectoryArm64 { get; }
 
        /// <summary>
        /// Path to the Sdks folder for this MSBuild instance.
        /// </summary>
        internal string MSBuildSDKsPath
        {
            get
            {
                string defaultSdkPath;
 
                if (VisualStudioInstallRootDirectory != null)
                {
                    // Can't use the N-argument form of Combine because it doesn't exist on .NET 3.5
                    defaultSdkPath = FileUtilities.CombinePaths(VisualStudioInstallRootDirectory, "MSBuild", "Sdks");
                }
                else
                {
                    defaultSdkPath = Path.Combine(CurrentMSBuildToolsDirectory, "Sdks");
                }
 
                // Allow an environment-variable override of the default SDK location
                return Environment.GetEnvironmentVariable("MSBuildSDKsPath") ?? defaultSdkPath;
            }
        }
 
        /// <summary>
        /// Full path to the current MSBuild configuration file.
        /// </summary>
        internal string CurrentMSBuildConfigurationFile { get; }
 
        /// <summary>
        /// Full path to current MSBuild.exe.
        /// <remarks>
        /// This path is likely not the current running process. We may be inside
        /// Visual Studio or a test harness. In that case this will point to the
        /// version of MSBuild found to be associated with the current environment.
        /// </remarks>
        /// </summary>
        internal string CurrentMSBuildExePath { get; private set; }
 
        /// <summary>
        /// Full path to the current MSBuild tools directory. This will be 32-bit unless
        /// we're executing from the 'AMD64' folder.
        /// </summary>
        internal string CurrentMSBuildToolsDirectory { get; }
 
        /// <summary>
        /// Path to the root Visual Studio install directory
        /// (e.g. 'C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise')
        /// </summary>
        internal string VisualStudioInstallRootDirectory { get; }
 
        /// <summary>
        /// MSBuild extensions path. On Standalone this defaults to the MSBuild folder. In
        /// VisualStudio mode this folder will be %VSINSTALLDIR%\MSBuild.
        /// </summary>
        internal string MSBuildExtensionsPath { get; set; }
    }
}