/// <summary> /// Create an MSBuild project collection. /// </summary> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <param name="globalPropertyOverrides"> /// An optional dictionary containing property values to override. /// </param> /// <returns> /// The project collection. /// </returns> public static ProjectCollection CreateProjectCollection(string solutionDirectory, Dictionary <string, string> globalPropertyOverrides = null) { return(CreateProjectCollection(solutionDirectory, DotNetRuntimeInfo.GetCurrent(solutionDirectory), globalPropertyOverrides )); }
/// <summary> /// Create global properties for MSBuild. /// </summary> /// <param name="runtimeInfo"> /// Information about the current .NET Core runtime. /// </param> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <returns> /// A dictionary containing the global properties. /// </returns> public static Dictionary <string, string> CreateGlobalMSBuildProperties(DotNetRuntimeInfo runtimeInfo, string solutionDirectory) { if (runtimeInfo == null) { throw new ArgumentNullException(nameof(runtimeInfo)); } if (String.IsNullOrWhiteSpace(solutionDirectory)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'solutionDirectory'.", nameof(solutionDirectory)); } if (solutionDirectory.Length > 0 && solutionDirectory[solutionDirectory.Length - 1] != Path.DirectorySeparatorChar) { solutionDirectory += Path.DirectorySeparatorChar; } return(new Dictionary <string, string> { [WellKnownPropertyNames.DesignTimeBuild] = "true", [WellKnownPropertyNames.BuildProjectReferences] = "false", [WellKnownPropertyNames.ResolveReferenceDependencies] = "true", [WellKnownPropertyNames.SolutionDir] = solutionDirectory, [WellKnownPropertyNames.MSBuildExtensionsPath] = runtimeInfo.BaseDirectory, [WellKnownPropertyNames.MSBuildSDKsPath] = Path.Combine(runtimeInfo.BaseDirectory, "Sdks"), [WellKnownPropertyNames.RoslynTargetsPath] = Path.Combine(runtimeInfo.BaseDirectory, "Roslyn") }); }
/// <summary> /// Create an MSBuild project collection. /// </summary> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <param name="globalPropertyOverrides"> /// An optional dictionary containing property values to override. /// </param> /// <param name="logger"> /// An optional <see cref="ILogger"/> to use for diagnostic purposes (if not specified, the static <see cref="Log.Logger"/> will be used). /// </param> /// <returns> /// The project collection. /// </returns> public static ProjectCollection CreateProjectCollection(string solutionDirectory, Dictionary <string, string> globalPropertyOverrides = null, ILogger logger = null) { if (logger == null) { logger = Log.Logger; } return(CreateProjectCollection(solutionDirectory, DotNetRuntimeInfo.GetCurrent(solutionDirectory, logger), globalPropertyOverrides )); }
/// <summary> /// Create global properties for MSBuild. /// </summary> /// <param name="runtimeInfo"> /// Information about the current .NET Core runtime. /// </param> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <param name="globalPropertyOverrides"> /// An optional dictionary containing property values to override. /// </param> /// <returns> /// A dictionary containing the global properties. /// </returns> public static Dictionary <string, string> CreateGlobalMSBuildProperties(DotNetRuntimeInfo runtimeInfo, string solutionDirectory, Dictionary <string, string> globalPropertyOverrides = null) { if (runtimeInfo == null) { throw new ArgumentNullException(nameof(runtimeInfo)); } if (String.IsNullOrWhiteSpace(solutionDirectory)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'solutionDirectory'.", nameof(solutionDirectory)); } if (solutionDirectory.Length > 0 && solutionDirectory[solutionDirectory.Length - 1] != Path.DirectorySeparatorChar) { solutionDirectory += Path.DirectorySeparatorChar; } // Support overriding of SDKs path. string sdksPath = Environment.GetEnvironmentVariable("MSBuildSDKsPath"); if (String.IsNullOrWhiteSpace(sdksPath)) { sdksPath = Path.Combine(runtimeInfo.BaseDirectory, "Sdks"); } var globalProperties = new Dictionary <string, string> { [WellKnownPropertyNames.DesignTimeBuild] = "true", [WellKnownPropertyNames.BuildProjectReferences] = "false", [WellKnownPropertyNames.ResolveReferenceDependencies] = "true", [WellKnownPropertyNames.SolutionDir] = solutionDirectory, [WellKnownPropertyNames.MSBuildExtensionsPath] = runtimeInfo.BaseDirectory, [WellKnownPropertyNames.MSBuildSDKsPath] = sdksPath, [WellKnownPropertyNames.RoslynTargetsPath] = Path.Combine(runtimeInfo.BaseDirectory, "Roslyn") }; if (globalPropertyOverrides != null) { foreach (string propertyName in globalPropertyOverrides.Keys) { globalProperties[propertyName] = globalPropertyOverrides[propertyName]; } } return(globalProperties); }
/// <summary> /// Create an MSBuild project collection. /// </summary> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <param name="runtimeInfo"> /// Information about the current .NET Core runtime. /// </param> /// <returns> /// The project collection. /// </returns> public static ProjectCollection CreateProjectCollection(string solutionDirectory, DotNetRuntimeInfo runtimeInfo) { if (String.IsNullOrWhiteSpace(solutionDirectory)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'baseDir'.", nameof(solutionDirectory)); } if (runtimeInfo == null) { throw new ArgumentNullException(nameof(runtimeInfo)); } if (String.IsNullOrWhiteSpace(runtimeInfo.BaseDirectory)) { throw new InvalidOperationException("Cannot determine base directory for .NET Core."); } Dictionary <string, string> globalProperties = CreateGlobalMSBuildProperties(runtimeInfo, solutionDirectory); EnsureMSBuildEnvironment(globalProperties); ProjectCollection projectCollection = new ProjectCollection(globalProperties) { IsBuildEnabled = false }; // Override toolset paths (for some reason these point to the main directory where the dotnet executable lives). Toolset toolset = projectCollection.GetToolset("15.0"); toolset = new Toolset( toolsVersion: "15.0", toolsPath: globalProperties["MSBuildExtensionsPath"], projectCollection: projectCollection, msbuildOverrideTasksPath: "" ); projectCollection.AddToolset(toolset); return(projectCollection); }
/// <summary> /// Create an MSBuild project collection. /// </summary> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <returns> /// The project collection. /// </returns> public static ProjectCollection CreateProjectCollection(string solutionDirectory) { return(CreateProjectCollection(solutionDirectory, DotNetRuntimeInfo.GetCurrent(solutionDirectory) )); }
/// <summary> /// Create an MSBuild project collection. /// </summary> /// <param name="solutionDirectory"> /// The base (i.e. solution) directory. /// </param> /// <param name="runtimeInfo"> /// Information about the current .NET Core runtime. /// </param> /// <param name="globalPropertyOverrides"> /// An optional dictionary containing property values to override. /// </param> /// <returns> /// The project collection. /// </returns> public static ProjectCollection CreateProjectCollection(string solutionDirectory, DotNetRuntimeInfo runtimeInfo, Dictionary <string, string> globalPropertyOverrides = null) { if (String.IsNullOrWhiteSpace(solutionDirectory)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'baseDir'.", nameof(solutionDirectory)); } if (runtimeInfo == null) { throw new ArgumentNullException(nameof(runtimeInfo)); } if (String.IsNullOrWhiteSpace(runtimeInfo.BaseDirectory)) { throw new InvalidOperationException("Cannot determine base directory for .NET Core (check the output of 'dotnet --info')."); } Dictionary <string, string> globalProperties = CreateGlobalMSBuildProperties(runtimeInfo, solutionDirectory, globalPropertyOverrides); EnsureMSBuildEnvironment(globalProperties); ProjectCollection projectCollection = new ProjectCollection(globalProperties) { IsBuildEnabled = false }; SemanticVersion netcoreVersion; if (!SemanticVersion.TryParse(runtimeInfo.Version, out netcoreVersion)) { throw new FormatException($"Cannot parse .NET Core version '{runtimeInfo.Version}' (does not appear to be a valid semantic version)."); } // For .NET Core 3.0 and newer, toolset version is simply "Current" instead of "15.0" (tintoy/msbuild-project-tools-vscode#46). string toolsVersion = netcoreVersion.Major < 3 ? "15.0" : "Current"; // Override toolset paths (for some reason these point to the main directory where the dotnet executable lives). Toolset toolset = projectCollection.GetToolset(toolsVersion); toolset = new Toolset(toolsVersion, toolsPath: runtimeInfo.BaseDirectory, projectCollection: projectCollection, msbuildOverrideTasksPath: "" ); // Other toolset versions won't be supported by the .NET Core SDK projectCollection.RemoveAllToolsets(); // TODO: Add configuration setting that enables user to configure custom toolsets. projectCollection.AddToolset(toolset); projectCollection.DefaultToolsVersion = toolsVersion; return(projectCollection); }
/// <summary> /// Get information about the current .NET Core runtime. /// </summary> /// <param name="baseDirectory"> /// An optional base directory where dotnet.exe should be run (this may affect the version it reports due to global.json). /// </param> /// <returns> /// A <see cref="DotNetRuntimeInfo"/> containing the runtime information. /// </returns> public static DotNetRuntimeInfo GetCurrent(string baseDirectory = null) { DotNetRuntimeInfo runtimeInfo = new DotNetRuntimeInfo(); Process dotnetInfoProcess = new Process { StartInfo = new ProcessStartInfo { FileName = "dotnet", WorkingDirectory = baseDirectory, Arguments = "--info", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }, EnableRaisingEvents = true }; using (dotnetInfoProcess) { // For logging purposes. string command = $"{dotnetInfoProcess.StartInfo.FileName} {dotnetInfoProcess.StartInfo.Arguments}"; // Buffer the output locally (otherwise, the process may hang if it fills up its STDOUT / STDERR buffer). StringBuilder localOutputBuffer = new StringBuilder(); dotnetInfoProcess.OutputDataReceived += (sender, args) => { lock (localOutputBuffer) { localOutputBuffer.AppendLine(args.Data); } }; dotnetInfoProcess.ErrorDataReceived += (sender, args) => { lock (localOutputBuffer) { localOutputBuffer.AppendLine(args.Data); } }; Log.Debug("Launching {Command}...", command); dotnetInfoProcess.Start(); Log.Debug("Launched {Command}. Waiting for process {TargetProcessId} to terminate...", command, dotnetInfoProcess.Id); // Asynchronously start reading from STDOUT / STDERR. dotnetInfoProcess.BeginOutputReadLine(); dotnetInfoProcess.BeginErrorReadLine(); try { dotnetInfoProcess.WaitForExit(milliseconds: 5000); } catch (TimeoutException exitTimedOut) { Log.Error(exitTimedOut, "Timed out after waiting 5 seconds for {Command} to exit.", command); throw new TimeoutException($"Timed out after waiting 5 seconds for '{command}' to exit.", exitTimedOut); } Log.Debug("{Command} terminated with exit code {ExitCode}.", command, dotnetInfoProcess.ExitCode); string processOutput; lock (localOutputBuffer) { processOutput = localOutputBuffer.ToString(); } // Only log output if there's a problem. if (dotnetInfoProcess.ExitCode != 0) { if (!String.IsNullOrWhiteSpace(processOutput)) { Log.Debug("{Command} returned the following text on STDOUT / STDERR.\n\n{DotNetInfoOutput:l}", command, processOutput); } else { Log.Debug("{Command} returned no output on STDOUT / STDERR."); } } using (StringReader bufferReader = new StringReader(processOutput)) { return(ParseDotNetInfoOutput(bufferReader)); } } }
/// <summary> /// Parse the output of "dotnet --info" into a <see cref="DotNetRuntimeInfo"/>. /// </summary> /// <param name="dotnetInfoOutput"> /// A <see cref="TextReader"/> containing the output of "dotnet --info". /// </param> /// <returns> /// The <see cref="DotNetRuntimeInfo"/>. /// </returns> public static DotNetRuntimeInfo ParseDotNetInfoOutput(TextReader dotnetInfoOutput) { if (dotnetInfoOutput == null) { throw new ArgumentNullException(nameof(dotnetInfoOutput)); } DotNetRuntimeInfo runtimeInfo = new DotNetRuntimeInfo(); DotnetInfoSection currentSection = DotnetInfoSection.Start; string currentLine; while ((currentLine = dotnetInfoOutput.ReadLine()) != null) { if (String.IsNullOrWhiteSpace(currentLine)) { continue; } if (!currentLine.StartsWith(" ") && currentLine.EndsWith(":")) { currentSection++; if (currentSection > DotnetInfoSection.RuntimeEnvironment) { break; } continue; } string[] property = currentLine.Split(new char[] { ':' }, count: 2); if (property.Length != 2) { continue; } property[0] = property[0].Trim(); property[1] = property[1].Trim(); switch (currentSection) { case DotnetInfoSection.ProductInformation: { switch (property[0]) { case "Version": { runtimeInfo.Version = property[1]; break; } } break; } case DotnetInfoSection.RuntimeEnvironment: { switch (property[0]) { case "Base Path": { runtimeInfo.BaseDirectory = property[1]; break; } case "RID": { runtimeInfo.RID = property[1]; break; } } break; } } } return(runtimeInfo); }
/// <summary> /// Find and use the latest version of the MSBuild engine compatible with the current SDK. /// </summary> /// <param name="baseDirectory"> /// An optional base directory where dotnet.exe should be run (this may affect the version it reports due to global.json). /// </param> /// <param name="logger"> /// An optional <see cref="ILogger"/> to use for diagnostic purposes (if not specified, the static <see cref="Log.Logger"/> will be used). /// </param> public static void DiscoverMSBuildEngine(string baseDirectory = null, ILogger logger = null) { if (MSBuildLocator.IsRegistered) { MSBuildLocator.Unregister(); } _registeredMSBuildInstance = null; // Assume working directory is VS code's current working directory (i.e. the workspace root). // // Really, until we figure out a way to change the version of MSBuild we're using after the server has started, // we're still going to have problems here. // // In the end we will probably wind up having to move all the MSBuild stuff out to a separate process, and use something like GRPC (or even Akka.NET's remoting) to communicate with it. // It can be stopped and restarted by the language server (even having different instances for different SDK / MSBuild versions). // // This will also ensure that the language server's model doesn't expose any MSBuild objects anywhere. // // For now, though, let's choose the dumb option. DotNetRuntimeInfo runtimeInfo = DotNetRuntimeInfo.GetCurrent(baseDirectory, logger); // SDK versions are in SemVer format... SemanticVersion targetSdkSemanticVersion; if (!SemanticVersion.TryParse(runtimeInfo.SdkVersion, out targetSdkSemanticVersion)) { throw new Exception($"Cannot determine SDK version information for current .NET SDK (located at '{runtimeInfo.BaseDirectory}')."); } // ...which MSBuildLocator does not understand. Version targetSdkVersion = new Version( major: targetSdkSemanticVersion.Major, minor: targetSdkSemanticVersion.Minor, build: targetSdkSemanticVersion.Patch ); var queryOptions = new VisualStudioInstanceQueryOptions { // We can only load the .NET Core MSBuild engine DiscoveryTypes = DiscoveryType.DotNetSdk }; VisualStudioInstance[] allInstances = MSBuildLocator .QueryVisualStudioInstances(queryOptions) .ToArray(); VisualStudioInstance latestInstance = allInstances .OrderByDescending(instance => instance.Version) .FirstOrDefault(instance => // We need a version of MSBuild for the currently-supported SDK instance.Version == targetSdkVersion ); if (latestInstance == null) { string foundVersions = String.Join(", ", allInstances.Select(instance => instance.Version)); throw new Exception($"Cannot locate MSBuild engine for .NET SDK v{targetSdkVersion}. This probably means that MSBuild Project Tools cannot find the MSBuild for the current project instance. It did find the following version(s), though: [{foundVersions}]."); } MSBuildLocator.RegisterInstance(latestInstance); _registeredMSBuildInstance = latestInstance; }
/// <summary> /// Get information about the current .NET Core runtime. /// </summary> /// <param name="baseDirectory"> /// An optional base directory where dotnet.exe should be run (this may affect the version it reports due to global.json). /// </param> /// <returns> /// A <see cref="DotNetRuntimeInfo"/> containing the runtime information. /// </returns> public static DotNetRuntimeInfo GetCurrent(string baseDirectory = null) { DotNetRuntimeInfo runtimeInfo = new DotNetRuntimeInfo(); Process dotnetInfoProcess = Process.Start(new ProcessStartInfo { FileName = "dotnet", WorkingDirectory = baseDirectory, Arguments = "--info", UseShellExecute = false, RedirectStandardOutput = true }); using (dotnetInfoProcess) { dotnetInfoProcess.WaitForExit(); string currentSection = null; string currentLine; while ((currentLine = dotnetInfoProcess.StandardOutput.ReadLine()) != null) { if (String.IsNullOrWhiteSpace(currentLine)) { continue; } if (!currentLine.StartsWith(" ")) { currentSection = currentLine; continue; } string[] property = currentLine.Split(new char[] { ':' }, count: 2); if (property.Length != 2) { continue; } property[0] = property[0].Trim(); property[1] = property[1].Trim(); switch (currentSection) { case "Product Information:": { switch (property[0]) { case "Version": { runtimeInfo.Version = property[1]; break; } } break; } case "Runtime Environment:": { switch (property[0]) { case "Base Path": { runtimeInfo.BaseDirectory = property[1]; break; } case "RID": { runtimeInfo.RID = property[1]; break; } } break; } } } } return(runtimeInfo); }