1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 using System; 5 using System.Diagnostics; 6 using System.IO; 7 using System.Linq; 8 using System.Collections.Generic; 9 using System.Text.RegularExpressions; 10 using System.Threading; 11 12 namespace Microsoft.Build.Shared 13 { 14 internal class BuildEnvironmentHelper 15 { 16 // Since this class is added as 'link' to shared source in multiple projects, 17 // MSBuildConstants.CurrentVisualStudioVersion is not available in all of them. 18 private const string CurrentVisualStudioVersion = "15.0"; 19 20 // MSBuildConstants.CurrentToolsVersion 21 private const string CurrentToolsVersion = "15.0"; 22 23 /// <summary> 24 /// Name of the Visual Studio (and Blend) process. 25 // VS ASP intellisense server fails without Microsoft.VisualStudio.Web.Host. Remove when issue fixed: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/574986 26 /// </summary> 27 private static readonly string[] s_visualStudioProcess = {"DEVENV", "BLEND", "Microsoft.VisualStudio.Web.Host"}; 28 29 /// <summary> 30 /// Name of the MSBuild process(es) 31 /// </summary> 32 private static readonly string[] s_msBuildProcess = {"MSBUILD", "MSBUILDTASKHOST"}; 33 34 /// <summary> 35 /// Name of MSBuild executable files. 36 /// </summary> 37 private static readonly string[] s_msBuildExeNames = { "MSBuild.exe", "MSBuild.dll" }; 38 39 /// <summary> 40 /// Gets the cached Build Environment instance. 41 /// </summary> 42 public static BuildEnvironment Instance 43 { 44 get 45 { 46 try 47 { 48 return BuildEnvironmentHelperSingleton.s_instance; 49 } 50 catch (TypeInitializationException e) 51 { 52 if (e.InnerException != null) 53 { 54 // Throw the error that caused the TypeInitializationException. 55 // (likely InvalidOperationException) 56 throw e.InnerException; 57 } 58 59 throw; 60 } 61 } 62 } 63 64 /// <summary> 65 /// Find the location of MSBuild.exe based on the current environment. 66 /// </summary> 67 /// <remarks> 68 /// This defines the order and precedence for various methods of discovering MSBuild and associated toolsets. 69 /// At a high level, an install under Visual Studio is preferred as the user may have SDKs installed to a 70 /// specific instance of Visual Studio and build will only succeed if we can discover those. See 71 /// https://github.com/Microsoft/msbuild/issues/1461 for details. 72 /// </remarks> 73 /// <returns>Build environment.</returns> Initialize()74 private static BuildEnvironment Initialize() 75 { 76 // See https://github.com/Microsoft/msbuild/issues/1461 for specification of ordering and details. 77 var possibleLocations = new Func<BuildEnvironment>[] 78 { 79 TryFromEnvironmentVariable, 80 TryFromVisualStudioProcess, 81 TryFromMSBuildProcess, 82 TryFromMSBuildAssembly, 83 TryFromDevConsole, 84 TryFromSetupApi, 85 TryFromAppContextBaseDirectory 86 }; 87 88 foreach (var location in possibleLocations) 89 { 90 var env = location(); 91 if (env != null) 92 return env; 93 } 94 95 // If we can't find a suitable environment, continue in the 'None' mode. If not running tests, 96 // we will use the current running process for the CurrentMSBuildExePath value. This is likely 97 // wrong, but many things use the CurrentMSBuildToolsDirectory value which must be set for basic 98 // functionality to work. 99 // 100 // If we are running tests, then the current running process may be a test runner located in the 101 // NuGet packages folder. So in that case, we use the location of the current assembly, which 102 // will be in the output path of the test project, which is what we want. 103 104 string msbuildExePath; 105 if (s_runningTests()) 106 { 107 msbuildExePath = typeof(BuildEnvironmentHelper).Assembly.Location; 108 } 109 else 110 { 111 msbuildExePath = s_getProcessFromRunningProcess(); 112 } 113 114 return new BuildEnvironment( 115 BuildEnvironmentMode.None, 116 msbuildExePath, 117 runningTests: s_runningTests(), 118 runningInVisualStudio: false, 119 visualStudioPath: null); 120 } 121 TryFromEnvironmentVariable()122 private static BuildEnvironment TryFromEnvironmentVariable() 123 { 124 var msBuildExePath = s_getEnvironmentVariable("MSBUILD_EXE_PATH"); 125 126 return TryFromStandaloneMSBuildExe(msBuildExePath); 127 } 128 TryFromVisualStudioProcess()129 private static BuildEnvironment TryFromVisualStudioProcess() 130 { 131 if (!NativeMethodsShared.IsWindows) 132 return null; 133 134 var vsProcess = s_getProcessFromRunningProcess(); 135 if (!IsProcessInList(vsProcess, s_visualStudioProcess)) return null; 136 137 var vsRoot = FileUtilities.GetFolderAbove(vsProcess, 3); 138 string msBuildExe = GetMSBuildExeFromVsRoot(vsRoot); 139 140 return new BuildEnvironment( 141 BuildEnvironmentMode.VisualStudio, 142 msBuildExe, 143 runningTests: false, 144 runningInVisualStudio: true, 145 visualStudioPath: vsRoot); 146 } 147 TryFromMSBuildProcess()148 private static BuildEnvironment TryFromMSBuildProcess() 149 { 150 var msBuildExe = s_getProcessFromRunningProcess(); 151 if (!IsProcessInList(msBuildExe, s_msBuildProcess)) return null; 152 153 // First check if we're in a VS installation 154 if (NativeMethodsShared.IsWindows && 155 Regex.IsMatch(msBuildExe, $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*MSBuild(?:TaskHost)?\.exe", RegexOptions.IgnoreCase)) 156 { 157 return new BuildEnvironment( 158 BuildEnvironmentMode.VisualStudio, 159 msBuildExe, 160 runningTests: false, 161 runningInVisualStudio: false, 162 visualStudioPath: GetVsRootFromMSBuildAssembly(msBuildExe)); 163 } 164 165 // Standalone mode running in MSBuild.exe 166 return new BuildEnvironment( 167 BuildEnvironmentMode.Standalone, 168 msBuildExe, 169 runningTests: false, 170 runningInVisualStudio: false, 171 visualStudioPath: null); 172 } 173 TryFromMSBuildAssembly()174 private static BuildEnvironment TryFromMSBuildAssembly() 175 { 176 var buildAssembly = s_getExecutingAssemblyPath(); 177 if (buildAssembly == null) return null; 178 179 // Check for MSBuild.[exe|dll] next to the current assembly 180 var msBuildExe = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.exe"); 181 var msBuildDll = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.dll"); 182 183 // First check if we're in a VS installation 184 if (NativeMethodsShared.IsWindows && 185 Regex.IsMatch(buildAssembly, $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*", RegexOptions.IgnoreCase)) 186 { 187 // In a Visual Studio path we must have MSBuild.exe 188 if (File.Exists(msBuildExe)) 189 { 190 return new BuildEnvironment( 191 BuildEnvironmentMode.VisualStudio, 192 msBuildExe, 193 runningTests: s_runningTests(), 194 runningInVisualStudio: false, 195 visualStudioPath: GetVsRootFromMSBuildAssembly(msBuildExe)); 196 } 197 } 198 199 // We're not in VS, check for MSBuild.exe / dll to consider this a standalone environment. 200 string msBuildPath = null; 201 if (File.Exists(msBuildExe)) msBuildPath = msBuildExe; 202 else if (File.Exists(msBuildDll)) msBuildPath = msBuildDll; 203 204 if (!string.IsNullOrEmpty(msBuildPath)) 205 { 206 // Standalone mode with toolset 207 return new BuildEnvironment( 208 BuildEnvironmentMode.Standalone, 209 msBuildPath, 210 runningTests: s_runningTests(), 211 runningInVisualStudio: false, 212 visualStudioPath: null); 213 } 214 215 return null; 216 217 } 218 TryFromDevConsole()219 private static BuildEnvironment TryFromDevConsole() 220 { 221 if (s_runningTests()) 222 { 223 // If running unit tests, then don't try to get the build environment from MSBuild installed on the machine 224 // (we should be using the locally built MSBuild instead) 225 return null; 226 } 227 228 // VSINSTALLDIR and VisualStudioVersion are set from the Developer Command Prompt. 229 var vsInstallDir = s_getEnvironmentVariable("VSINSTALLDIR"); 230 var vsVersion = s_getEnvironmentVariable("VisualStudioVersion"); 231 232 if (string.IsNullOrEmpty(vsInstallDir) || string.IsNullOrEmpty(vsVersion) || 233 vsVersion != CurrentVisualStudioVersion || !Directory.Exists(vsInstallDir)) return null; 234 235 return new BuildEnvironment( 236 BuildEnvironmentMode.VisualStudio, 237 GetMSBuildExeFromVsRoot(vsInstallDir), 238 runningTests: false, 239 runningInVisualStudio: false, 240 visualStudioPath: vsInstallDir); 241 } 242 TryFromSetupApi()243 private static BuildEnvironment TryFromSetupApi() 244 { 245 if (s_runningTests()) 246 { 247 // If running unit tests, then don't try to get the build environment from MSBuild installed on the machine 248 // (we should be using the locally built MSBuild instead) 249 return null; 250 } 251 252 Version v = new Version(CurrentVisualStudioVersion); 253 var instances = s_getVisualStudioInstances() 254 .Where(i => i.Version.Major == v.Major && Directory.Exists(i.Path)) 255 .ToList(); 256 257 if (instances.Count == 0) return null; 258 259 if (instances.Count > 1) 260 { 261 // TODO: Warn user somehow. We may have picked the wrong one. 262 } 263 264 return new BuildEnvironment( 265 BuildEnvironmentMode.VisualStudio, 266 GetMSBuildExeFromVsRoot(instances[0].Path), 267 runningTests: false, 268 runningInVisualStudio: false, 269 visualStudioPath: instances[0].Path); 270 } 271 TryFromAppContextBaseDirectory()272 private static BuildEnvironment TryFromAppContextBaseDirectory() 273 { 274 // Assemblies compiled against anything older than .NET 4.0 won't have a System.AppContext 275 // Try the base directory that the assembly resolver uses to probe for assemblies. 276 // Under certain scenarios the assemblies are loaded from spurious locations like the NuGet package cache 277 // but the toolset files are copied to the app's directory via "contentFiles". 278 279 var appContextBaseDirectory = s_getAppContextBaseDirectory(); 280 if (string.IsNullOrEmpty(appContextBaseDirectory)) return null; 281 282 // Look for possible MSBuild exe names in the AppContextBaseDirectory 283 return s_msBuildExeNames 284 .Select((name) => TryFromStandaloneMSBuildExe(Path.Combine(appContextBaseDirectory, name))) 285 .FirstOrDefault(env => env != null); 286 } 287 TryFromStandaloneMSBuildExe(string msBuildExePath)288 private static BuildEnvironment TryFromStandaloneMSBuildExe(string msBuildExePath) 289 { 290 if (!string.IsNullOrEmpty(msBuildExePath) && File.Exists(msBuildExePath)) 291 { 292 // MSBuild.exe was found outside of Visual Studio. Assume Standalone mode. 293 return new BuildEnvironment( 294 BuildEnvironmentMode.Standalone, 295 msBuildExePath, 296 runningTests: s_runningTests(), 297 runningInVisualStudio: false, 298 visualStudioPath: null); 299 } 300 301 return null; 302 } 303 GetVsRootFromMSBuildAssembly(string msBuildAssembly)304 private static string GetVsRootFromMSBuildAssembly(string msBuildAssembly) 305 { 306 return FileUtilities.GetFolderAbove(msBuildAssembly, 307 Regex.IsMatch(msBuildAssembly, $@".\\MSBuild\\{CurrentToolsVersion}\\Bin\\Amd64\\MSBuild\.exe", RegexOptions.IgnoreCase) 308 ? 5 309 : 4); 310 } 311 GetMSBuildExeFromVsRoot(string visualStudioRoot)312 private static string GetMSBuildExeFromVsRoot(string visualStudioRoot) 313 { 314 return FileUtilities.CombinePaths(visualStudioRoot, "MSBuild", CurrentToolsVersion, "Bin", "MSBuild.exe"); 315 } 316 317 private static bool? _runningTests; 318 private static readonly object _runningTestsLock = new object(); 319 CheckIfRunningTests()320 private static bool CheckIfRunningTests() 321 { 322 if (_runningTests != null) 323 { 324 return _runningTests.Value; 325 } 326 327 lock (_runningTestsLock) 328 { 329 if (_runningTests != null) 330 { 331 return _runningTests.Value; 332 } 333 334 // Check if running tests via the TestInfo class in Microsoft.Build.Framework. 335 // See the comments on the TestInfo class for an explanation of why it works this way. 336 var frameworkAssembly = typeof(Framework.ITask).Assembly; 337 var testInfoType = frameworkAssembly.GetType("Microsoft.Build.Framework.TestInfo"); 338 var runningTestsField = testInfoType.GetField("s_runningTests", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); 339 340 _runningTests = (bool)runningTestsField.GetValue(null); 341 342 return _runningTests.Value; 343 } 344 } 345 346 /// <summary> 347 /// Returns true if processName appears in the processList 348 /// </summary> 349 /// <param name="processName">Name of the process</param> 350 /// <param name="processList">List of processes to check</param> 351 /// <returns></returns> IsProcessInList(string processName, string[] processList)352 private static bool IsProcessInList(string processName, string[] processList) 353 { 354 var processFileName = Path.GetFileNameWithoutExtension(processName); 355 356 if (string.IsNullOrEmpty(processFileName)) 357 { 358 return false; 359 } 360 361 return processList.Any(s => 362 processFileName.Equals(s, StringComparison.OrdinalIgnoreCase)); 363 } 364 GetProcessFromRunningProcess()365 private static string GetProcessFromRunningProcess() 366 { 367 #if RUNTIME_TYPE_NETCORE 368 return AssemblyUtilities.GetAssemblyLocation(AssemblyUtilities.EntryAssembly); 369 #else 370 return Process.GetCurrentProcess().MainModule.FileName; 371 #endif 372 } 373 GetExecutingAssemblyPath()374 private static string GetExecutingAssemblyPath() 375 { 376 return FileUtilities.ExecutingAssemblyPath; 377 } 378 GetAppContextBaseDirectory()379 private static string GetAppContextBaseDirectory() 380 { 381 #if !CLR2COMPATIBILITY // Assemblies compiled against anything older than .NET 4.0 won't have a System.AppContext 382 return AppContext.BaseDirectory; 383 #else 384 return null; 385 #endif 386 } 387 GetEnvironmentVariable(string variable)388 private static string GetEnvironmentVariable(string variable) 389 { 390 return Environment.GetEnvironmentVariable(variable); 391 } 392 393 /// <summary> 394 /// Resets the current singleton instance (for testing). 395 /// </summary> 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)396 internal static void ResetInstance_ForUnitTestsOnly(Func<string> getProcessFromRunningProcess = null, 397 Func<string> getExecutingAssemblyPath = null, Func<string> getAppContextBaseDirectory = null, 398 Func<IEnumerable<VisualStudioInstance>> getVisualStudioInstances = null, 399 Func<string, string> getEnvironmentVariable = null, 400 Func<bool> runningTests = null) 401 { 402 s_getProcessFromRunningProcess = getProcessFromRunningProcess ?? GetProcessFromRunningProcess; 403 s_getExecutingAssemblyPath = getExecutingAssemblyPath ?? GetExecutingAssemblyPath; 404 s_getAppContextBaseDirectory = getAppContextBaseDirectory ?? GetAppContextBaseDirectory; 405 s_getVisualStudioInstances = getVisualStudioInstances ?? VisualStudioLocationHelper.GetInstances; 406 s_getEnvironmentVariable = getEnvironmentVariable ?? GetEnvironmentVariable; 407 408 // Tests which specifically test the BuildEnvironmentHelper need it to be able to act as if it is not running tests 409 s_runningTests = runningTests ?? CheckIfRunningTests; 410 411 BuildEnvironmentHelperSingleton.s_instance = Initialize(); 412 } 413 414 private static Func<string> s_getProcessFromRunningProcess = GetProcessFromRunningProcess; 415 private static Func<string> s_getExecutingAssemblyPath = GetExecutingAssemblyPath; 416 private static Func<string> s_getAppContextBaseDirectory = GetAppContextBaseDirectory; 417 private static Func<IEnumerable<VisualStudioInstance>> s_getVisualStudioInstances = VisualStudioLocationHelper.GetInstances; 418 private static Func<string, string> s_getEnvironmentVariable = GetEnvironmentVariable; 419 private static Func<bool> s_runningTests = CheckIfRunningTests; 420 421 422 private static class BuildEnvironmentHelperSingleton 423 { 424 // Explicit static constructor to tell C# compiler 425 // not to mark type as beforefieldinit BuildEnvironmentHelperSingleton()426 static BuildEnvironmentHelperSingleton() 427 { } 428 429 public static BuildEnvironment s_instance = Initialize(); 430 } 431 } 432 433 /// <summary> 434 /// Enum which defines which environment / mode MSBuild is currently running. 435 /// </summary> 436 internal enum BuildEnvironmentMode 437 { 438 /// <summary> 439 /// Running from Visual Studio directly or from MSBuild installed under an instance of Visual Studio. 440 /// Toolsets and extensions will be loaded from the Visual Studio instance. 441 /// </summary> 442 VisualStudio, 443 444 /// <summary> 445 /// Running in a standalone toolset mode. All toolsets and extensions paths are relative to the app 446 /// running and not dependent on Visual Studio. (e.g. dotnet CLI, open source clone of our repo) 447 /// </summary> 448 Standalone, 449 450 /// <summary> 451 /// Running without any defined toolsets. Most functionality limited. Likely will not be able to 452 /// build or evaluate a project. (e.g. reference to Microsoft.*.dll without a toolset definition 453 /// or Visual Studio instance installed). 454 /// </summary> 455 None 456 } 457 458 /// <summary> 459 /// Defines the current environment for build tools. 460 /// </summary> 461 internal class BuildEnvironment 462 { BuildEnvironment(BuildEnvironmentMode mode, string currentMSBuildExePath, bool runningTests, bool runningInVisualStudio, string visualStudioPath)463 public BuildEnvironment(BuildEnvironmentMode mode, string currentMSBuildExePath, bool runningTests, bool runningInVisualStudio, string visualStudioPath) 464 { 465 FileInfo currentMSBuildExeFile = null; 466 DirectoryInfo currentToolsDirectory = null; 467 468 Mode = mode; 469 RunningTests = runningTests; 470 RunningInVisualStudio = runningInVisualStudio; 471 CurrentMSBuildExePath = currentMSBuildExePath; 472 VisualStudioInstallRootDirectory = visualStudioPath; 473 474 if (!string.IsNullOrEmpty(currentMSBuildExePath)) 475 { 476 currentMSBuildExeFile = new FileInfo(currentMSBuildExePath); 477 currentToolsDirectory = currentMSBuildExeFile.Directory; 478 479 CurrentMSBuildToolsDirectory = currentMSBuildExeFile.DirectoryName; 480 CurrentMSBuildConfigurationFile = string.Concat(currentMSBuildExePath, ".config"); 481 MSBuildToolsDirectory32 = CurrentMSBuildToolsDirectory; 482 MSBuildToolsDirectory64 = CurrentMSBuildToolsDirectory; 483 } 484 485 // We can't detect an environment, don't try to set other paths. 486 if (mode == BuildEnvironmentMode.None || currentMSBuildExeFile == null || currentToolsDirectory == null) 487 return; 488 489 // Check to see if our current folder is 'amd64' 490 bool runningInAmd64 = string.Equals(currentToolsDirectory.Name, "amd64", StringComparison.OrdinalIgnoreCase); 491 492 var msBuildExeName = currentMSBuildExeFile.Name; 493 var folderAbove = currentToolsDirectory.Parent?.FullName; 494 495 if (folderAbove != null) 496 { 497 // Calculate potential paths to other architecture MSBuild.exe 498 var potentialAmd64FromX86 = FileUtilities.CombinePaths(CurrentMSBuildToolsDirectory, "amd64", msBuildExeName); 499 var potentialX86FromAmd64 = Path.Combine(folderAbove, msBuildExeName); 500 501 // Check for existence of an MSBuild file. Note this is not necessary in a VS installation where we always want to 502 // assume the correct layout. 503 var existsCheck = mode == BuildEnvironmentMode.VisualStudio ? new Func<string, bool>(_ => true) : File.Exists; 504 505 // Running in amd64 folder and the X86 path is valid 506 if (runningInAmd64 && existsCheck(potentialX86FromAmd64)) 507 { 508 MSBuildToolsDirectory32 = folderAbove; 509 MSBuildToolsDirectory64 = CurrentMSBuildToolsDirectory; 510 } 511 // Not running in amd64 folder and the amd64 path is valid 512 else if (!runningInAmd64 && existsCheck(potentialAmd64FromX86)) 513 { 514 MSBuildToolsDirectory32 = CurrentMSBuildToolsDirectory; 515 MSBuildToolsDirectory64 = Path.Combine(CurrentMSBuildToolsDirectory, "amd64"); 516 } 517 } 518 519 MSBuildExtensionsPath = mode == BuildEnvironmentMode.VisualStudio 520 ? Path.Combine(VisualStudioInstallRootDirectory, "MSBuild") 521 : MSBuildToolsDirectory32; 522 } 523 524 internal BuildEnvironmentMode Mode { get; } 525 526 /// <summary> 527 /// Gets the flag that indicates if we are running in a test harness. 528 /// </summary> 529 internal bool RunningTests { get; } 530 531 /// <summary> 532 /// Returns true when the entry point application is Visual Studio. 533 /// </summary> 534 internal bool RunningInVisualStudio { get; } 535 536 /// <summary> 537 /// Path to the MSBuild 32-bit tools directory. 538 /// </summary> 539 internal string MSBuildToolsDirectory32 { get; } 540 541 /// <summary> 542 /// Path to the MSBuild 64-bit (AMD64) tools directory. 543 /// </summary> 544 internal string MSBuildToolsDirectory64 { get; } 545 546 /// <summary> 547 /// Path to the Sdks folder for this MSBuild instance. 548 /// </summary> 549 internal string MSBuildSDKsPath 550 { 551 get 552 { 553 string defaultSdkPath; 554 555 if (VisualStudioInstallRootDirectory != null) 556 { 557 // Can't use the N-argument form of Combine because it doesn't exist on .NET 3.5 558 defaultSdkPath = FileUtilities.CombinePaths(VisualStudioInstallRootDirectory, "MSBuild", "Sdks"); 559 } 560 else 561 { 562 defaultSdkPath = Path.Combine(CurrentMSBuildToolsDirectory, "Sdks"); 563 } 564 565 // Allow an environment-variable override of the default SDK location 566 return Environment.GetEnvironmentVariable("MSBuildSDKsPath") ?? defaultSdkPath; 567 } 568 } 569 570 /// <summary> 571 /// Full path to the current MSBuild configuration file. 572 /// </summary> 573 internal string CurrentMSBuildConfigurationFile { get; } 574 575 /// <summary> 576 /// Full path to current MSBuild.exe. 577 /// <remarks> 578 /// This path is likely not the current running process. We may be inside 579 /// Visual Studio or a test harness. In that case this will point to the 580 /// version of MSBuild found to be associated with the current environment. 581 /// </remarks> 582 /// </summary> 583 internal string CurrentMSBuildExePath { get; private set; } 584 585 /// <summary> 586 /// Full path to the current MSBuild tools directory. This will be 32-bit unless 587 /// we're executing from the 'AMD64' folder. 588 /// </summary> 589 internal string CurrentMSBuildToolsDirectory { get; } 590 591 /// <summary> 592 /// Path to the root Visual Studio install directory 593 /// (e.g. 'c:\Program Files (x86)\Microsoft Visual Studio 15.0') 594 /// </summary> 595 internal string VisualStudioInstallRootDirectory { get; } 596 597 /// <summary> 598 /// MSBuild extensions path. On Standalone this defaults to the MSBuild folder. In 599 /// VisualStudio mode this folder will be %VSINSTALLDIR%\MSBuild. 600 /// </summary> 601 internal string MSBuildExtensionsPath { get; set; } 602 } 603 } 604