using Microsoft.CodeAnalysis; using Microsoft.VisualStudio.Setup.Configuration; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; namespace Codelyzer.Analysis.Common { /// /// Detects the MSBuild Path that best matches the solution /// public class MSBuildDetector { private List _visualStudioInstances; private const string SolutionFileVSVersion = "VisualStudioVersion"; public class VisualStudioInstanceData { public VisualStudioInstanceData() { Workloads = new List(); } public string Id { get; set; } public string Name { get; set; } public string Version { get; set; } public string InstallationPath { get; set; } public List Workloads { get; set; } public string VisualStudioMSBuildPath { get; set; } } public MSBuildDetector() { _visualStudioInstances = GetVisualStudioInstallations(); } public List GetVisualStudioInstanceData() => _visualStudioInstances; public string MatchSolution(string solutionPath) { var version = GetVisualStudioVersionNumber(solutionPath); var solutionMajorVersion = GetMajorVersion(version); var firstInstanceWithMatchingVersion = _visualStudioInstances?.OrderByDescending(v => v.Version).FirstOrDefault(vr => solutionMajorVersion == GetMajorVersion(vr.Version)); //If there's a visual studio solution that matches the one used to create the solution: if (firstInstanceWithMatchingVersion != null) { return firstInstanceWithMatchingVersion.VisualStudioMSBuildPath; } //If no version matches, return the max version else { return _visualStudioInstances?.OrderByDescending(v => v.Version).FirstOrDefault().VisualStudioMSBuildPath; } } private int GetMajorVersion(string version) { int index = version.IndexOf('.'); if(index < 0) { index = version.Length; } return int.Parse(version.Substring(0, index)); } private string GetVisualStudioVersionNumber(string solutionPath) { var solutionFileText = File.ReadAllText(solutionPath); var lines = solutionFileText.Split(Environment.NewLine); var constantLine = lines.FirstOrDefault(l => l.StartsWith(SolutionFileVSVersion)); var minNumberArray = constantLine.Split("="); return minNumberArray[minNumberArray.Length - 1].Trim(); } public List GetVisualStudioInstallations() { var visualStudioInstances = new List(); try { var query = new SetupConfiguration(); var query2 = (ISetupConfiguration2)query; var e = query2.EnumAllInstances(); int fetched; var instances = new ISetupInstance[1]; do { e.Next(1, instances, out fetched); if (fetched > 0) { var visualStudioInstance = ParseVisualStudioInstanceData(instances[0]); visualStudioInstances.Add(visualStudioInstance); } } while (fetched > 0); } catch (Exception ex) { //Visual studio is not installed } if (!visualStudioInstances.Any()) { var msbuildExes = GetFileSystemMsBuildExePath()?.ToList(); msbuildExes.ForEach(msbuildExe => { visualStudioInstances.Add(new VisualStudioInstanceData() { Name = "MSBuild", VisualStudioMSBuildPath = msbuildExe }); }); } return visualStudioInstances; } private VisualStudioInstanceData ParseVisualStudioInstanceData(ISetupInstance instance) { var visualStudioInstanceData = new VisualStudioInstanceData(); var instance2 = (ISetupInstance2)instance; var state = instance2.GetState(); if (state == InstanceState.Complete) { visualStudioInstanceData.Id = instance2.GetProduct().GetId(); visualStudioInstanceData.Name = instance2.GetDisplayName(); visualStudioInstanceData.Version = instance.GetInstallationVersion(); visualStudioInstanceData.InstallationPath = instance2.GetInstallationPath(); var packages = instance2.GetPackages()?.ToList(); packages.Where(package => package.GetType() == "Workload")?.ToList() .ForEach(package => { visualStudioInstanceData.Workloads.Add(package.GetId()); }); var msBuildPackage = packages.FirstOrDefault(package => package.GetId().Equals("Microsoft.Component.MSBuild", StringComparison.InvariantCultureIgnoreCase)); if(msBuildPackage != null) { var msBuildBinDir = Path.Combine(visualStudioInstanceData.InstallationPath, "MSBuild", "Current", "Bin"); if (Directory.Exists(msBuildBinDir)) { visualStudioInstanceData.VisualStudioMSBuildPath = Path.Combine(msBuildBinDir, "MSBuild.exe"); } } } return visualStudioInstanceData; } public string GetFirstMatchingMsBuildFromPath(string programFilesPath = null, string programFilesX86Path = null, string toolsVersion = null) => GetFileSystemMsBuildExePath(programFilesPath, programFilesX86Path, toolsVersion).FirstOrDefault(); public List GetFileSystemMsBuildExePath(string programFilesPath = null, string programFilesX86Path = null, string toolsVersion = null) { // Could not find the tools path, possibly due to https://github.com/Microsoft/msbuild/issues/2369 // Try to poll for it. From https://github.com/KirillOsenkov/MSBuildStructuredLog/blob/4649f55f900a324421bad5a714a2584926a02138/src/StructuredLogViewer/MSBuildLocator.cs var result = new List(); List editions = new List { "Enterprise", "Professional", "Community", "BuildTools" }; var targets = new string[] { "Microsoft.CSharp.targets", "Microsoft.CSharp.CurrentVersion.targets", "Microsoft.Common.targets" }; DirectoryInfo vsDirectory; var programFiles = programFilesPath ?? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string programFilesX86 = programFilesX86Path ?? System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86); //2022 vsDirectory = new DirectoryInfo(Path.Combine(programFiles, "Microsoft Visual Studio")); // "Microsoft.CSharp.CrossTargeting.targets" string msbuildpath = GetMsBuildPathFromVSDirectory(vsDirectory, editions, targets, toolsVersion); if (!string.IsNullOrEmpty(msbuildpath)) { result.Add(msbuildpath); } // 2019, 2017 vsDirectory = new DirectoryInfo(Path.Combine(programFilesX86, "Microsoft Visual Studio")); msbuildpath = GetMsBuildPathFromVSDirectory(vsDirectory, editions, targets, toolsVersion); if (!string.IsNullOrEmpty(msbuildpath)) { result.Add(msbuildpath); } // 14.0, 12.0 vsDirectory = new DirectoryInfo(Path.Combine(programFilesX86, "MSBuild")); msbuildpath = GetMsBuildPathFromVSDirectoryBelow15(vsDirectory, editions, targets); if (!string.IsNullOrEmpty(msbuildpath)) { result.Add(msbuildpath); } return result; } public static string GetMsBuildPathFromVSDirectory(DirectoryInfo vsDirectory, List editions, string[] targets, string projectToolsVersion) { try { TryParseVersionString(projectToolsVersion, out double projectMsbuildVersionNumber); if (vsDirectory.Exists) { List msBuildExePath = vsDirectory .GetDirectories("MSBuild", SearchOption.AllDirectories) .SelectMany(msBuildDir => msBuildDir.GetFiles("MSBuild.exe", SearchOption.AllDirectories)) .OrderByDescending(msbuild => FileVersionInfo.GetVersionInfo(msbuild.FullName).FileVersion) .ThenBy(msbuild => editions.IndexOf(GetEditionType(msbuild.DirectoryName, editions))) .ThenByDescending(msbuild => { var folderName = GetVersionFolder(msbuild.FullName); // Prioritize any "current" version first if (folderName.Equals("current", StringComparison.OrdinalIgnoreCase)) return double.MaxValue; // Prioritize in version order or last if the version number cannot be parsed return TryParseVersionString(folderName, out var folderVersion) ? folderVersion : double.MinValue; }) .Where(msbuild => { var targetsWithPath = GetTargetsWithPath(msbuild.DirectoryName, targets); if (targetsWithPath.TrueForAll(File.Exists)) return true; return false; }) .ToList(); if (projectMsbuildVersionNumber > 0) { // If we have a tools version, use that to remove any versions of msbuild that are earlier than the project version msBuildExePath = msBuildExePath?.Where( msbuild => { var fileVersion = FileVersionInfo.GetVersionInfo(msbuild.FullName).FileVersion; return TryParseVersionString(fileVersion, out var msBuildVersion) && msBuildVersion >= projectMsbuildVersionNumber; } )?.ToList(); } return msBuildExePath?.FirstOrDefault()?.FullName; }; return null; } catch(Exception ex) { return null; } } private string GetMsBuildPathFromVSDirectoryBelow15(DirectoryInfo vsDirectory, List editions, string[] targets) { if (vsDirectory.Exists) { List msBuildExePath = vsDirectory .GetFiles("MSBuild.exe", SearchOption.AllDirectories) .OrderByDescending(msbuild => FileVersionInfo.GetVersionInfo(msbuild.FullName).FileVersion) .ThenByDescending(msbuild => { var folderName = GetVersionFolder(msbuild.FullName); // Prioritize any "current" version first if (folderName.Equals("current", StringComparison.OrdinalIgnoreCase)) return double.MaxValue; // Prioritize in version order or last if the version number cannot be parsed return TryParseVersionString(folderName, out var msBuildVersion) ? msBuildVersion : double.MinValue; }) .Where(msbuild => { var targetsWithPath = GetTargetsWithPath(msbuild.DirectoryName, targets); return targetsWithPath.TrueForAll(File.Exists); }) .ToList(); return msBuildExePath?.FirstOrDefault()?.FullName; }; return ""; } private static string GetEditionType(string vsPath, List editions) { string[] elements = vsPath.Split(Path.DirectorySeparatorChar); foreach (var edition in editions) { if (elements.Contains(edition)) { return edition; } } return ""; } private static string GetVersionFolder(string vsPath) { List elements = vsPath.Split(Path.DirectorySeparatorChar).ToList(); var folderIdx = elements.IndexOf("MSBuild"); return elements[folderIdx + 1]; } private static List GetTargetsWithPath(string vsPath, string[] targets) { List targetsWithPath = new List(); foreach (string target in targets) { targetsWithPath.Add(Path.Combine(vsPath, target)); } return targetsWithPath; } private static bool TryParseVersionString(string s, out double version) { if (string.IsNullOrEmpty(s)) { version = 0; return false; } // Try parsing as a proper version string if (Version.TryParse(s, out var parsedVersion)) { // Convert "Major.Minor" to a double if (parsedVersion.Minor > 0) { var minor = (double)parsedVersion.Minor; version = parsedVersion.Major + minor / Math.Pow(10, 1 + Math.Floor(Math.Log10(minor))); } else { version = parsedVersion.Major; } return true; } // Fallback: parse as double in case it's only a single integer return double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out version); } } }