/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs
445 строк
19 KB
Jan Jones
Allow MSBuildWorkspace to open file-based apps (#84139)
30 июл 2026, 11:03
Не верифицирован
30 июл 2026, 11:03
7e7ebef
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Roslyn.Utilities; using MSB = Microsoft.Build; namespace Microsoft.CodeAnalysis.MSBuild; internal sealed class ProjectBuildManager : IDisposable { private static readonly XmlReaderSettings s_xmlReaderSettings = new() { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; private static readonly Dictionary<string, string> s_defaultGlobalProperties = new() { // this will tell msbuild to not build the dependent projects { PropertyNames.DesignTimeBuild, bool.TrueString }, // this will force CoreCompile task to execute even if all inputs and outputs are up to date #if NET { PropertyNames.NonExistentFile, "__NonExistentSubDir__\\__NonExistentFile__" }, #else // Setting `BuildingInsideVisualStudio` indirectly sets NonExistentFile: // https://github.com/microsoft/msbuild/blob/ab9b2f36a5ff7a85f842b205d5529e77fdc9d7ab/src/Tasks/Microsoft.Common.CurrentVersion.targets#L3462-L3470 { PropertyNames.BuildingInsideVisualStudio, bool.TrueString }, #endif { PropertyNames.BuildProjectReferences, bool.FalseString }, { PropertyNames.BuildingProject, bool.FalseString }, // retrieve the command-line arguments to the compiler { PropertyNames.ProvideCommandLineArgs, bool.TrueString }, // don't actually run the compiler { PropertyNames.SkipCompilerExecution, bool.TrueString }, { PropertyNames.ContinueOnError, PropertyValues.ErrorAndContinue }, // this ensures that the parent project's configuration and platform will be used for // referenced projects. So, setting Configuration=Release will also cause any project // references to also be built with Configuration=Release. This is necessary for getting // a more-likely-to-be-correct output path from project references. { PropertyNames.ShouldUnsetParentConfigurationAndPlatform, bool.FalseString } }; public string[] KnownCommandLineParserLanguages { get; } private readonly MSB.Evaluation.ProjectCollection _projectCollection; private readonly MSBuildDiagnosticLogger _buildLogger = new() { Verbosity = MSB.Framework.LoggerVerbosity.Normal }; private readonly MSB.Execution.BuildParameters _buildParameters; /// <summary> /// An object used as a monitor to guard <see cref="_activeBuilds"/> and <see cref="_unloadedProjectsSinceLastCacheReset"/>. /// For us to call <see cref="MSB.Execution.BuildManager.ResetCaches"/>; we need to end builds, call ResetCaches, and then begin build again. /// This lets us easily count how many active builds there are, wait until that hits zero, and then block new builds from starting until /// we're done with the reset. /// /// It's unclear if we could do a simpler approach taking advantage of the fact that EndBuild() will block on running builds, but it appears /// submissions that aren't actually started will not get blocked for, which we need. /// </summary> private readonly object _activeBuildsLock = new object(); private int _activeBuilds; private int _unloadedProjectsSinceLastCacheReset; private bool _disposed; public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<string, string> globalProperties, ILogger? msbuildLogger = null, int? maxNodeCount = null) { KnownCommandLineParserLanguages = knownCommandLineParserLanguages; var allProperties = new Dictionary<string, string>(s_defaultGlobalProperties); foreach (var kvp in globalProperties) allProperties[kvp.Key] = kvp.Value; // Pass in the binlog (if any) to the ProjectCollection to ensure evaluation results are included in it. // // We do not need to include the _buildLogger in the ProjectCollection - it just collects the // DiagnosticLog from the build steps, but evaluation already separately reports the DiagnosticLog. var loggers = msbuildLogger is not null ? new MSB.Framework.ILogger[] { msbuildLogger } : Array.Empty<MSB.Framework.ILogger>(); // Pass empty loggers array to workaround LoggerException when passing binary logger to both evaluation and build. See https://github.com/dotnet/msbuild/issues/11867 _projectCollection = new MSB.Evaluation.ProjectCollection(allProperties, loggers: [], MSB.Evaluation.ToolsetDefinitionLocations.Default); _buildParameters = new MSB.Execution.BuildParameters(_projectCollection) { // The loggers are not inherited from the project collection, so specify both the // binlog logger and the _buildLogger for the build steps. Loggers = [.. loggers, _buildLogger], // If we have an additional logger and it's diagnostic, then we need to opt into task inputs globally, or otherwise // it won't get any log events. This logic matches https://github.com/dotnet/msbuild/blob/fa6710d2720dcf1230a732a8858ffe71bcdbe110/src/Build/Instance/ProjectInstance.cs#L2365-L2371 LogTaskInputs = msbuildLogger is not null && msbuildLogger.Verbosity == LoggerVerbosity.Diagnostic, // Disable node reuse so nodes don't live around once we're done EnableNodeReuse = false }; if (maxNodeCount is int nodeCount) _buildParameters.MaxNodeCount = nodeCount; MSB.Execution.BuildManager.DefaultBuildManager.BeginBuild(_buildParameters); } public async Task<(MSB.Evaluation.Project? project, DiagnosticLog log)> LoadProjectAsync( string path, CancellationToken cancellationToken) { Contract.ThrowIfTrue(_disposed); var log = new DiagnosticLog(); try { var loadedProjects = _projectCollection.GetLoadedProjects(path); if (loadedProjects.Count > 0) { Debug.Assert(loadedProjects.Count == 1); return (loadedProjects.First(), log); } using var stream = FileUtilities.OpenAsyncRead(path); using var readStream = new MemoryStream(); // There is no overload that we can call that takes a cancellationToken but doesn't take a bufferSize; this bufferSize // is the default if we call the overload with just a stream. await stream.CopyToAsync(readStream, bufferSize: 81920, cancellationToken).ConfigureAwait(false); readStream.Position = 0; return LoadProjectCore(path, readStream, globalProperties: null, log); } catch (Exception e) { log.Add(e, path); return (project: null, log); } } private (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProjectCore( string path, Stream readStream, IDictionary<string, string>? globalProperties, DiagnosticLog log) { try { using var xmlReader = XmlReader.Create(readStream, s_xmlReaderSettings); var xml = MSB.Construction.ProjectRootElement.Create(xmlReader, _projectCollection); // When constructing a project from an XmlReader, MSBuild cannot determine the project file path. Setting the // path explicitly is necessary so that the reserved properties like $(MSBuildProjectDirectory) will work. xml.FullPath = path; // Roughly matches the VS project load settings for their design time builds. var projectLoadSettings = MSB.Evaluation.ProjectLoadSettings.RejectCircularImports | MSB.Evaluation.ProjectLoadSettings.IgnoreEmptyImports | MSB.Evaluation.ProjectLoadSettings.IgnoreMissingImports | MSB.Evaluation.ProjectLoadSettings.IgnoreInvalidImports | MSB.Evaluation.ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition | MSB.Evaluation.ProjectLoadSettings.FailOnUnresolvedSdk; var project = new MSB.Evaluation.Project( xml, globalProperties, toolsVersion: null, _projectCollection, projectLoadSettings); return (project, log); } catch (Exception e) { log.Add(e, path); return (project: null, log); } } public (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProject(string path, Stream readStream, IDictionary<string, string>? globalProperties) { Contract.ThrowIfTrue(_disposed); var log = new DiagnosticLog(); try { return LoadProjectCore(path, readStream, globalProperties, log); } catch (Exception e) { log.Add(e, path); return (project: null, log); } } public (MSB.Execution.ProjectInstance? projectInstance, DiagnosticLog log) LoadProjectInstance(string path, TextReader content, IDictionary<string, string>? additionalGlobalProperties) { Contract.ThrowIfTrue(_disposed); var log = new DiagnosticLog(); try { using var xmlReader = XmlReader.Create(content, s_xmlReaderSettings); var projectRootElement = MSB.Construction.ProjectRootElement.Create(xmlReader, _projectCollection); projectRootElement.FullPath = path; var mergedGlobalProperties = new Dictionary<string, string>(_projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); if (additionalGlobalProperties != null) { foreach (var pair in additionalGlobalProperties) { mergedGlobalProperties[pair.Key] = pair.Value; } } var projectInstance = MSB.Execution.ProjectInstance.FromProjectRootElement(projectRootElement, new MSB.Definition.ProjectOptions { ProjectCollection = _projectCollection, GlobalProperties = mergedGlobalProperties, }); return (projectInstance, log); } catch (Exception e) { log.Add(e, path); return (projectInstance: null, log); } } public async Task<string?> TryGetOutputFilePathAsync( string path, CancellationToken cancellationToken) { // This tries to get the project output path and retrieving the evaluated $(TargetPath) property. var (project, _) = await LoadProjectAsync(path, cancellationToken).ConfigureAwait(false); return project?.GetPropertyValue(PropertyNames.TargetPath); } public void Dispose() { if (_disposed) return; MSB.Execution.BuildManager.DefaultBuildManager.EndBuild(); // unload project so collection will release global strings _projectCollection.UnloadAllProjects(); _projectCollection.Dispose(); _disposed = true; } public async Task<MSB.Execution.ProjectInstance[]> BuildProjectInstancesAsync( MSB.Evaluation.Project project, DiagnosticLog log, CancellationToken cancellationToken) { Contract.ThrowIfTrue(_disposed); var targetFrameworkValue = project.GetPropertyValue(PropertyNames.TargetFramework); var targetFrameworksValue = project.GetPropertyValue(PropertyNames.TargetFrameworks); if (!RoslynString.IsNullOrEmpty(targetFrameworkValue) || RoslynString.IsNullOrEmpty(targetFrameworksValue)) { return [await BuildProjectInstanceAsync(project, log, cancellationToken).ConfigureAwait(false)]; } // This project has a <TargetFrameworks> property, but does not specify a <TargetFramework>. // In this case, we need to iterate through the <TargetFrameworks>, set <TargetFramework> with // each value, and build the project. var targetFrameworks = targetFrameworksValue.Split(';'); if (!project.GlobalProperties.TryGetValue(PropertyNames.TargetFramework, out var initialGlobalTargetFrameworkValue)) initialGlobalTargetFrameworkValue = null; var results = new List<MSB.Execution.ProjectInstance>(targetFrameworks.Length); foreach (var targetFramework in targetFrameworks) { project.SetGlobalProperty(PropertyNames.TargetFramework, targetFramework); project.ReevaluateIfNecessary(); var projectInstance = await BuildProjectInstanceAsync(project, log, cancellationToken).ConfigureAwait(false); results.Add(projectInstance); } if (initialGlobalTargetFrameworkValue is null) { project.RemoveGlobalProperty(PropertyNames.TargetFramework); } else { project.SetGlobalProperty(PropertyNames.TargetFramework, initialGlobalTargetFrameworkValue); } project.ReevaluateIfNecessary(); return results.ToArray(); } private Task<MSB.Execution.ProjectInstance> BuildProjectInstanceAsync( MSB.Evaluation.Project project, DiagnosticLog log, CancellationToken cancellationToken) { var requiredTargets = new[] { TargetNames.Compile, TargetNames.CoreCompile }; var optionalTargets = new[] { TargetNames.DesignTimeMarkupCompilation }; return BuildProjectInstanceAsync(project, requiredTargets, optionalTargets, log, cancellationToken); } private async Task<MSB.Execution.ProjectInstance> BuildProjectInstanceAsync( MSB.Evaluation.Project project, string[] requiredTargets, string[] optionalTargets, DiagnosticLog log, CancellationToken cancellationToken) { // create a project instance to be executed by build engine. // The executed project will hold the final model of the project after execution via msbuild. var projectInstance = project.CreateProjectInstance(); // Verify targets foreach (var target in requiredTargets) { if (!projectInstance.Targets.ContainsKey(target)) { log.Add(string.Format(WorkspaceMSBuildBuildHostResources.Project_does_not_contain_0_target, target), projectInstance.FullPath); return projectInstance; } } var targets = new List<string>(requiredTargets); foreach (var target in optionalTargets) { if (projectInstance.Targets.ContainsKey(target)) { targets.Add(target); } } // Since we are doing parallel builds where the build may happen in another MSBuild node, we need to pass // ProvideProjectStateAfterBuild to get that state back when we're done. var buildRequestData = new MSB.Execution.BuildRequestData( projectInstance, [.. targets], hostServices: null, flags: MSB.Execution.BuildRequestDataFlags.ProvideProjectStateAfterBuild); var result = await BuildAsync(buildRequestData, log, cancellationToken).ConfigureAwait(false); if (result.OverallResult == MSB.Execution.BuildResultCode.Failure) { if (result.Exception != null) { log.Add(result.Exception, projectInstance.FullPath); } } return result.ProjectStateAfterBuild ?? projectInstance; } private async Task<MSB.Execution.BuildResult> BuildAsync(MSB.Execution.BuildRequestData requestData, DiagnosticLog log, CancellationToken cancellationToken) { // MSBuild doesn't have a way to cancel a single submission, so we'll only check the token before we start. In practice this is fine -- // the RPC layer we use to call into the BuildHost doesn't support cancellation anyways so there's no reason to have lots of extra code. cancellationToken.ThrowIfCancellationRequested(); // Before we start a build, record that one is running; this will block if we're currently clearing caches. lock (_activeBuildsLock) { _activeBuilds++; } BuildSubmission? submission = null; try { submission = MSB.Execution.BuildManager.DefaultBuildManager.PendBuildRequest(requestData); // The SubmissionId is assigned by PendBuildRequest and is the same SubmissionId that appears on the // BuildEventContext of every event raised while this submission builds. _buildLogger.RegisterLog(submission.SubmissionId, log); var taskSource = new TaskCompletionSource<MSB.Execution.BuildResult>(); // Start the job submission.ExecuteAsync(sub => { // when finished try { var result = sub.BuildResult; taskSource.TrySetResult(result); } catch (Exception e) { taskSource.TrySetException(e); } }, null); return await taskSource.Task.ConfigureAwait(false); } finally { lock (_activeBuildsLock) { _activeBuilds--; if (_activeBuilds == 0) Monitor.PulseAll(_activeBuildsLock); } // Ensure the log is cleaned up if it's still there no matter if we take an exceptional path or not if (submission is not null) _buildLogger.TryUnregisterLog(submission.SubmissionId); } } public void UnloadProject(MSB.Evaluation.Project project) { _projectCollection.UnloadProject(project); lock (_activeBuildsLock) { // If we've unloaded a lot of projects since our last reset, let's just reset it. 500 is chosen with no evidence // whatsoever. if (++_unloadedProjectsSinceLastCacheReset >= 500) { _unloadedProjectsSinceLastCacheReset = 0; // We need to wait until there are no active builds before we reset caches while (_activeBuilds > 0) Monitor.Wait(_activeBuildsLock); // No active builds, so clear caches MSB.Execution.BuildManager.DefaultBuildManager.EndBuild(); MSB.Execution.BuildManager.DefaultBuildManager.ResetCaches(); MSB.Execution.BuildManager.DefaultBuildManager.BeginBuild(_buildParameters); } } } }