|
// 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.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 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);
var 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, log);
}
catch (Exception e)
{
log.Add(e, path);
return (project: null, log);
}
}
private (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProjectCore(
string path, Stream readStream, 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: null,
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)
{
Contract.ThrowIfTrue(_disposed);
var log = new DiagnosticLog();
try
{
return LoadProjectCore(path, readStream, log);
}
catch (Exception e)
{
log.Add(e, path);
return (project: 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);
}
}
var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, [.. targets]);
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 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();
var submission = MSB.Execution.BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
try
{
// 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
{
// Ensure the log is cleaned up if it's still there no matter if we take an exceptional path or not
_buildLogger.TryUnregisterLog(submission.SubmissionId);
}
}
}
|