File: Build\ProjectBuildManager.cs
Web Access
Project: src\src\sdk\src\Dotnet.Watch\Watch\Microsoft.DotNet.HotReload.Watch.csproj (Microsoft.DotNet.HotReload.Watch)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;

namespace Microsoft.DotNet.Watch;

internal sealed class ProjectBuildManager(ProjectCollection collection, BuildReporter reporter)
{
    /// <summary>
    /// Semaphore that ensures we only start one build build at a time per process, which is required by MSBuild.
    /// </summary>
    private static readonly SemaphoreSlim s_buildSemaphore = new(initialCount: 1);

    private static readonly IReadOnlyDictionary<string, TargetResult> s_emptyTargetResults = new Dictionary<string, TargetResult>();

    public readonly ProjectCollection Collection = collection;
    public readonly BuildReporter BuildReporter = reporter;

    /// <summary>
    /// Used by tests to ensure no more than one build is running at a time, which is required by MSBuild.
    /// </summary>
    internal static SemaphoreSlim Test_BuildSemaphore
        => s_buildSemaphore;

    /// <summary>
    /// Executes the specified build requests.
    /// </summary>
    /// <param name="onFailure">Invoked for each project that fails to build. Returns true to continue build or false to cancel.</param>
    /// <returns>True if all projects built successfully.</returns>
    public async Task<ImmutableArray<BuildResult<T>>> BuildAsync<T>(
        IReadOnlyList<BuildRequest<T>> requests,
        Func<ProjectInstance, bool> onFailure,
        string operationName,
        CancellationToken cancellationToken)
    {
        Debug.Assert(requests is not []);
        var buildRequests = requests.Select(r => new BuildRequestData(r.ProjectInstance, [.. r.Targets])).ToArray();

        using var loggers = BuildReporter.GetLoggers(buildRequests[0].ProjectFullPath, operationName);

        using var buildCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        await s_buildSemaphore.WaitAsync(cancellationToken);

        var manager = BuildManager.DefaultBuildManager;
        using var _ = buildCancellationTokenSource.Token.Register(manager.CancelAllSubmissions);

        var buildParameters = new BuildParameters(Collection)
        {
            Loggers = loggers,
        };

        manager.BeginBuild(buildParameters);
        try
        {
            var buildTasks = new List<Task<BuildResult?>>(buildRequests.Length);

            foreach (var request in buildRequests)
            {
                var taskSource = new TaskCompletionSource<BuildResult?>();

                // Queues the build request and immediately returns. The callback is executed when the build completes.
                manager.PendBuildRequest(request).ExecuteAsync(
                    callback: submission =>
                    {
                        // Cancel on first failure:
                        if (submission.BuildResult?.OverallResult != BuildResultCode.Success)
                        {
                            var projectInstance = (ProjectInstance)submission.AsyncContext!;

                            var continueBuild = onFailure(projectInstance);
                            if (!continueBuild)
                            {
                                buildCancellationTokenSource.Cancel();
                                taskSource.SetCanceled();
                                return;
                            }
                        }

                        taskSource.SetResult(submission.BuildResult);
                    },
                    context: request.ProjectInstance);

                buildTasks.Add(taskSource.Task);
            }

            var results = await Task.WhenAll(buildTasks);

            return [.. results.Select((result, index) => new BuildResult<T>(
                (IReadOnlyDictionary<string, TargetResult>?)result?.ResultsByTarget ?? s_emptyTargetResults,
                requests[index].ProjectInstance,
                requests[index].Data))];
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            // build was canceled
            loggers.ReportOutput();
            return [];
        }
        finally
        {
            manager.EndBuild();
            s_buildSemaphore.Release();
        }
    }
}