File: Utilities\ToolLocationHelper_VisualStudio.cs
Web Access
Project: Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;

#nullable disable

namespace Microsoft.Build.Utilities;

public static partial class ToolLocationHelper
{
    /// <summary>
    /// Cache the VS Install folders for particular range of VS versions
    /// </summary>
    private static Dictionary<string, string[]> s_vsInstallFolders;

    /// <summary>
    ///  Returns folders in VS installs of specifid range of versions, starting with 15.0.
    /// </summary>
    /// <param name="minVersion">Optional. If specified, only VS instances with this or bigger version will be included.</param>
    /// <param name="maxVersion">Optional. If specified, only VS instances with smaller versions will be included. For instance, 16.0 means that only 15.* versions will be included.</param>
    /// <param name="subFolder">Optional. If specified, the returned list will contain [VSInstallDir][subFolder] paths</param>
    /// <returns>Returns folders in VS installs in VS version descending order</returns>
    public static IEnumerable<string> GetFoldersInVSInstalls(Version minVersion = null, Version maxVersion = null, string subFolder = null)
    {
        string versionRange = $"{minVersion?.ToString() ?? string.Empty};{maxVersion?.ToString() ?? string.Empty}";
        string[] vsInstallFolders;

        lock (s_locker)
        {
            s_vsInstallFolders ??= new Dictionary<string, string[]>();

            if (!s_vsInstallFolders.TryGetValue(versionRange, out vsInstallFolders))
            {
                IList<VisualStudioInstance> vsInstances = VisualStudioLocationHelper.GetInstances();

                var vsInstancePaths = vsInstances
                    .Where(i => (minVersion == null || i.Version >= minVersion) && (maxVersion == null || i.Version < maxVersion))
                    .OrderByDescending(i => i.Version)
                    .Select(i => i.Path);

                vsInstallFolders = vsInstancePaths.ToArray();
                s_vsInstallFolders[versionRange] = vsInstallFolders;
            }
        }

        var folders = string.IsNullOrEmpty(subFolder) ? vsInstallFolders : vsInstallFolders.Select(i => Path.Combine(i, subFolder));

        return folders;
    }

    /// <summary>
    ///  Returns folders in VS installs of specified range of versions (starting with 15.0) separated by ';'.
    /// </summary>
    /// <param name="minVersionString">Optional. If specified, only VS instances with this or bigger version will be included.</param>
    /// <param name="maxVersionString">Optional. If specified, only VS instances with smaller versions will be included. For instance, 16.0 means that only 15.* versions will be included.</param>
    /// <param name="subFolder">Optional. If specified, the returned list will contain [VSInstallDir][subFolder] paths</param>
    /// <returns>Returns folders in VS installs in VS version descending order separated by ';'</returns>
    public static string GetFoldersInVSInstallsAsString(string minVersionString = null, string maxVersionString = null, string subFolder = null)
    {
        string foldersString = string.Empty;

        try
        {
            Version minVersion, maxVersion;

            if (string.IsNullOrEmpty(minVersionString) || !Version.TryParse(minVersionString, out minVersion))
            {
                minVersion = null;
            }
            if (string.IsNullOrEmpty(maxVersionString) || !Version.TryParse(maxVersionString, out maxVersion))
            {
                maxVersion = null;
            }

            var folders = GetFoldersInVSInstalls(minVersion, maxVersion, subFolder);

            if (folders.Any())
            {
                foldersString = string.Join(";", folders);
            }
        }
        catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
        {
            // This method will be used in vc props and we don't want to fail project load if it throws for some non critical reason.
        }

        return foldersString;
    }

    /// <summary>
    /// Finds first folder in the list which contains all given files. Returns an empty string if not found.
    /// </summary>
    /// <param name="possibleRoots">Root folders separated by ';'</param>
    /// <param name="relativeFilePaths">Relative file paths to find under root folders, separated by ';'.</param>
    /// <returns>The first root folder in the given list, which contains all files. Empty string if not found.</returns>
    public static string FindRootFolderWhereAllFilesExist(string possibleRoots, string relativeFilePaths)
    {
        if (!string.IsNullOrEmpty(possibleRoots))
        {
            var roots = possibleRoots.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
            var files = relativeFilePaths.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);

            bool allFilesFound;
            foreach (var root in roots)
            {
                allFilesFound = true;
                foreach (var file in files)
                {
                    try
                    {
                        string fullPath = Path.Combine(root, file);

                        if (!FileSystems.Default.FileExists(fullPath))
                        {
                            allFilesFound = false;
                            break;
                        }
                    }
                    catch (ArgumentException)
                    {
                        allFilesFound = false;
                        break;
                    }
                }

                if (allFilesFound)
                {
                    return root;
                }
            }
        }

        return string.Empty;
    }
}