File: DeltaProject.cs
Web Access
Project: src\src\runtime\src\tools\hotreload-delta-gen\Microsoft.DotNet.HotReload.Utils.Generator\Microsoft.DotNet.HotReload.Utils.Generator.csproj (Microsoft.DotNet.HotReload.Utils.Generator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.DotNet.HotReload.Utils.Generator;

/// Drives the creation of deltas from textual changes.
internal class DeltaProject
{
    readonly HotReloadService _hotReloadService;

    readonly Solution _solution;
    readonly ProjectId _baseProjectId;
    readonly DeltaNaming _nextName;

    public DeltaProject(BaselineArtifacts artifacts) {
        _hotReloadService = artifacts.HotReloadService;
        _solution = artifacts.BaselineSolution;
        _baseProjectId = artifacts.BaselineProjectId;
        _nextName = new DeltaNaming(artifacts.BaselineOutputAsmPath, 1);
    }

    internal DeltaProject (DeltaProject prev, Solution newSolution)
    {
        _hotReloadService = prev._hotReloadService;
        _solution = newSolution;
        _baseProjectId = prev._baseProjectId;
        _nextName = prev._nextName.Next ();
    }

    public Solution Solution => _solution;

    public ProjectId BaseProjectId => _baseProjectId;

    /// The default output function
    ///  Creates files with the specified DeltaNaming without any other side-effects
    public static DeltaOutputStreams DefaultMakeFileOutputs (DeltaNaming dinfo) {
        var metaStream = File.Create(dinfo.Dmeta);
        var ilStream = File.Create(dinfo.Dil);
        var pdbStream = File.Create(dinfo.Dpdb);
        var updateHandlerInfoStream = File.Create(dinfo.UpdateHandlerInfo);
        return new DeltaOutputStreams(metaStream, ilStream, pdbStream, updateHandlerInfoStream);
    }

    /// Builds a delta for the specified document given a path to its updated contents and a revision count
    /// On failure throws a DiffyException and with exitStatus > 0
    public async Task<DeltaProject> BuildDelta (Delta delta, bool ignoreUnchanged = false,
                                                        Func<DeltaNaming, DeltaOutputStreams>? makeOutputs = default,
                                                        Action<DeltaNaming, DeltaOutputStreams>? outputsReady = default,
                                                        CancellationToken ct = default)
    {
        var change = delta.Change;
        var dinfo = _nextName;

        Console.WriteLine ($"parsing patch #{dinfo.Rev} from {change.Update} and creating delta");

        Project oldProject = Solution.GetProject(BaseProjectId)!;

        DocumentId baseDocumentId = change.Document;

        Document oldDocument = oldProject.GetDocument(baseDocumentId)!;

        Document updatedDocument;
        Solution updatedSolution;
        await using (var contents = File.OpenRead (change.Update)) {
            updatedSolution = Solution.WithDocumentText (baseDocumentId, SourceText.From (contents, Encoding.UTF8));
            updatedDocument = updatedSolution.GetDocument(baseDocumentId)!;
        }
        if (updatedDocument.Project.Id != BaseProjectId)
            throw new Exception ("Unexpectedly, project Id of the delta != base project Id");
        if (updatedDocument.Id != baseDocumentId)
            throw new Exception ("Unexpectedly, document Id of the delta != base document Id");

        var changes = await updatedDocument.GetTextChangesAsync (oldDocument, ct);
        if (!changes.Any()) {
            Console.WriteLine ("no changes found");
            if (ignoreUnchanged)
                return this;
            //FIXME can continue here and just ignore the revision
            throw new DiffyException ($"no changes in revision {dinfo.Rev}", exitStatus: 5);
        }

        Console.WriteLine ($"Found changes in {oldDocument.Name}");

        var updates = await _hotReloadService.GetUpdatesAsync (updatedSolution, runningProjects: [], ct);

        if (updates.PersistentDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) {
            var sb = new StringBuilder();
            foreach (var diag in updates.PersistentDiagnostics) {
                sb.AppendLine (diag.ToString ());
            }
            throw new DiffyException ($"Failed to emit delta for {oldDocument.Name}: {sb}", exitStatus: 8);
        }

        foreach (var fancyChange in updates.ProjectUpdates)
        {
            Console.WriteLine("change service made {0}", fancyChange.ModuleId);
        }

        _hotReloadService.CommitUpdate();

        await using (var output = makeOutputs != null ?  makeOutputs(dinfo) : DefaultMakeFileOutputs(dinfo)) {
            if (updates.ProjectUpdates.Length != 1) {
                throw new DiffyException($"Expected only one module in the delta, got {updates.ProjectUpdates.Length}", exitStatus: 10);
            }
            var update = updates.ProjectUpdates.First();
            output.MetaStream.Write(update.MetadataDelta.AsSpan());
            output.IlStream.Write(update.ILDelta.AsSpan());
            output.PdbStream.Write(update.PdbDelta.AsSpan());
            System.Text.Json.JsonSerializer.Serialize(output.UpdateHandlerInfoStream, new UpdateHandlerInfo (update.UpdatedTypes));
            outputsReady?.Invoke(dinfo, output);
        }
        Console.WriteLine($"wrote {dinfo.Dmeta}");
        // return a new deltaproject that can build the next update
        return new DeltaProject(this, updatedSolution);
    }

}