diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index b0c686bbfc8b89..3804c0bab259d7 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -45,8 +45,6 @@ This file should be imported by eng/Versions.props
11.0.0-preview.4.26203.108
11.0.0-preview.4.26203.108
11.0.0-preview.4.26203.108
-
- 11.0.0-alpha.0.26173.1
11.0.0-alpha.1.26181.1
@@ -159,8 +157,6 @@ This file should be imported by eng/Versions.props
$(SystemReflectionMetadataPackageVersion)
$(SystemReflectionMetadataLoadContextPackageVersion)
$(SystemTextJsonPackageVersion)
-
- $(MicrosoftDotNetHotReloadUtilsGeneratorBuildToolPackageVersion)
$(MicrosoftNETCoreRuntimeICUTransportPackageVersion)
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index e08d4138b67887..b54a2950d99b4f 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -319,10 +319,6 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-optimization
bc6c6a6b3ae72cfa214ec44b992a60aca978f176
-
- https://github.com/dotnet/hotreload-utils
- f5f73b933b5c2a37a4048a49bc2b1a6a0bf2c693
-
https://github.com/dotnet/runtime-assets
509fb52c027cd46fab093f10c89691cda982edc4
diff --git a/eng/testing/hotreload-delta-gen.targets b/eng/testing/hotreload-delta-gen.targets
new file mode 100644
index 00000000000000..28ee99934fe6ce
--- /dev/null
+++ b/eng/testing/hotreload-delta-gen.targets
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+ dotnet
+
+ <_HotReloadDeltaGeneratorToolDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'Microsoft.DotNet.HotReload.Utils.Generator.BuildTool', '$(Configuration)', '$(NetCoreAppToolCurrent)'))
+
+ <_HotReloadDeltaGeneratorPath Condition="'$(_HotReloadDeltaGeneratorPath)' == ''">$(_HotReloadDeltaGeneratorToolDir)Microsoft.DotNet.HotReload.Utils.Generator.BuildTool.dll
+
+ <_HotReloadDeltaGeneratorCommand>"$(DotNetTool)" "$(_HotReloadDeltaGeneratorPath)"
+
+ <_HotReloadDeltaGeneratorTasksDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'Microsoft.DotNet.HotReload.Utils.Generator.Tasks', '$(Configuration)', '$(NetCoreAppToolCurrent)'))
+
+ <_HotReloadDeltaGeneratorTasksPath Condition="'$(_HotReloadDeltaGeneratorTasksPath)' == ''">$(_HotReloadDeltaGeneratorTasksDir)Microsoft.DotNet.HotReload.Utils.Generator.Tasks.dll
+
+
+
+ <_HotReloadDeltaGeneratorDeltaScript Condition="'$(DeltaScript)' != ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(DeltaScript)'))
+
+
+
+ <_HotReloadDeltaGeneratorShouldRun Condition="'$(_HotReloadDeltaGeneratorShouldRun)' == '' and Exists('$(_HotReloadDeltaGeneratorDeltaScript)')">true
+ <_HotReloadDeltaGeneratorShouldRun Condition="'$(_HotReloadDeltaGeneratorShouldRun)' == ''">false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_HotReloadDeltaGeneratorArgs>"-msbuild:$(MSBuildProjectFullPath)"
+ <_HotReloadDeltaGeneratorArgs>$(_HotReloadDeltaGeneratorArgs) "-script:$(_HotReloadDeltaGeneratorDeltaScript)"
+
+
+ <_HotReloadDeltaGeneratorArgs Condition="'$(Configuration)' != ''">$(_HotReloadDeltaGeneratorArgs) -p:Configuration=$(Configuration)
+ <_HotReloadDeltaGeneratorArgs Condition="'$(RuntimeIdentifier)' != ''">$(_HotReloadDeltaGeneratorArgs) -p:RuntimeIdentifier=$(RuntimeIdentifier)
+
+ <_HotReloadDeltaGeneratorArgs Condition="'$(HotReloadDeltaGeneratorExtraArgs)' != ''">$(_HotReloadDeltaGeneratorArgs) $(HotReloadDeltaGeneratorExtraArgs)
+
+
+
+
+
+
+
+ always
+
+
+
+
+
diff --git a/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.props b/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.props
index 52577ca88d57a7..0781b64f323591 100644
--- a/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.props
+++ b/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.props
@@ -1,17 +1,6 @@
-
-
-
-
-
false
diff --git a/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.targets b/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.targets
index 11832c6fa179de..5db1f406dfaa1c 100644
--- a/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.targets
+++ b/src/libraries/System.Runtime.Loader/tests/ApplyUpdate/Directory.Build.targets
@@ -1,3 +1,4 @@
+
diff --git a/src/libraries/pretest.proj b/src/libraries/pretest.proj
index ab577965d36c54..7b53dffc05900c 100644
--- a/src/libraries/pretest.proj
+++ b/src/libraries/pretest.proj
@@ -20,6 +20,10 @@
+
+
+
+
diff --git a/src/mono/sample/mbr/console/ConsoleDelta.csproj b/src/mono/sample/mbr/console/ConsoleDelta.csproj
index 86fb68ec7f4808..ab6059bed9810d 100644
--- a/src/mono/sample/mbr/console/ConsoleDelta.csproj
+++ b/src/mono/sample/mbr/console/ConsoleDelta.csproj
@@ -17,20 +17,17 @@
-
-
-
+
+ deltascript.json
+
+
+
-
-
- deltascript.json
-
-
$(HotReloadDeltaGeneratorExtraArgs) -p:BuiltRuntimeConfiguration=$(BuiltRuntimeConfiguration)
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj
index 9b185656b973f4..205331daef1547 100644
--- a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj
+++ b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj
@@ -19,14 +19,6 @@
-
-
-
-
+
diff --git a/src/tools/hotreload-delta-gen/Common/TempDirectory.cs b/src/tools/hotreload-delta-gen/Common/TempDirectory.cs
new file mode 100644
index 00000000000000..62543590e50aed
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Common/TempDirectory.cs
@@ -0,0 +1,28 @@
+// 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.IO;
+
+namespace Microsoft.DotNet.HotReload.Utils.Common;
+
+internal class TempDirectory : IDisposable
+{
+ public TempDirectory(bool keep = false, string? dirname = null)
+ {
+ string subdir = dirname ?? System.IO.Path.GetRandomFileName();
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), subdir);
+ Keep = keep;
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+ public bool Keep { get; set; }
+
+ public void Dispose()
+ {
+ if (!Keep)
+ Directory.Delete(Path, true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Directory.Build.props b/src/tools/hotreload-delta-gen/Directory.Build.props
new file mode 100644
index 00000000000000..f9076932426279
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Directory.Build.props
@@ -0,0 +1,17 @@
+
+
+
+
+ $(NetCoreAppToolCurrent)
+ enable
+ Latest
+ tools\hotreload-delta-gen\
+ false
+
+
+ false
+
+
diff --git a/src/tools/hotreload-delta-gen/Directory.Build.targets b/src/tools/hotreload-delta-gen/Directory.Build.targets
new file mode 100644
index 00000000000000..5d4870b330a002
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool.csproj b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool.csproj
new file mode 100644
index 00000000000000..04453bd3ff2455
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ Major
+
+
+
+
+
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Program.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Program.cs
new file mode 100644
index 00000000000000..9d345fdc156218
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Program.cs
@@ -0,0 +1,4 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+return Microsoft.DotNet.HotReload.Utils.Generator.Frontend.Frontend.Main(args);
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/README.md b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/README.md
new file mode 100644
index 00000000000000..e5fa0dd89d1f71
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/README.md
@@ -0,0 +1,31 @@
+# Microsoft.DotNet.HotReload.Utils.Generator.BuildTool #
+
+Generate deltas as part of an MSBuild project.
+
+## How to use it ##
+
+Starting with an existing SDK-style project, add:
+
+```xml
+
+
+
+
+
+ deltascript.json
+
+```
+
+Where the `deltascript.json` file contains the changes to be applied:
+
+```json
+{"changes":
+ [
+ {"document": "relativePath/to/file.cs", "update": "relativePath/to/file_v1.cs"},
+ {"document": "file2.cs", "update": "file2_v2.cs"},
+ {"document": "relativePath/to/file.cs", "update": "relativePath/to/file_v3.cs"}
+ ]
+}
+```
+
+The tool will run as part of the build after the `Build` target and generate `.dmeta`, `.dil` and `.dpdb` files in `$(OutputPath)`.
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Microsoft.DotNet.HotReload.Utils.Generator.Data.csproj b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Microsoft.DotNet.HotReload.Utils.Generator.Data.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Microsoft.DotNet.HotReload.Utils.Generator.Data.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/OutputSummary/OutputSummary.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/OutputSummary/OutputSummary.cs
new file mode 100644
index 00000000000000..33547e6d69c328
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/OutputSummary/OutputSummary.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Serialization;
+
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.OutputSummary;
+
+public class OutputSummary {
+ [JsonPropertyName("deltas")]
+ public Delta[]? Deltas {get; init;}
+
+ [JsonExtensionData]
+ public System.Collections.Generic.Dictionary? Extra {get; init;}
+
+ [JsonConstructor]
+ public OutputSummary(Delta[]? deltas) {
+ Deltas = deltas;
+ }
+}
+
+public class Delta {
+ [JsonPropertyName("assembly")]
+ public string? Assembly {get; init;}
+ [JsonPropertyName("metadata")]
+ public string? Metadata {get; init;}
+ [JsonPropertyName("il")]
+ public string? IL {get; init;}
+ [JsonPropertyName("pdb")]
+ public string? Pdb {get; init;}
+
+ [JsonConstructor]
+ public Delta (string? assembly, string? metadata, string? il, string? pdb) {
+ Assembly = assembly;
+ Metadata = metadata;
+ IL = il;
+ Pdb = pdb;
+ }
+
+ [JsonExtensionData]
+ public System.Collections.Generic.Dictionary? Extra {get; set;}
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/Script.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/Script.cs
new file mode 100644
index 00000000000000..1a24353d5421ea
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/Script.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Script.Json;
+public class Script {
+
+ [JsonConverter(typeof(ScriptCapabilitiesConverter))]
+ public string? Capabilities {get; init;}
+ public Change[]? Changes {get; init;}
+
+ [System.Text.Json.Serialization.JsonConstructor]
+ public Script (string? capabilities, Change[]? changes) {
+ Capabilities = capabilities;
+ Changes = changes;
+ }
+
+}
+
+public class Change {
+ public string Document {get; init;}
+ public string Update {get; init;}
+
+ [System.Text.Json.Serialization.JsonConstructor]
+ public Change (string document, string update) {
+ Document = document;
+ Update = update;
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/ScriptCapabilitiesConverter.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/ScriptCapabilitiesConverter.cs
new file mode 100644
index 00000000000000..e31a79ed6b1a6e
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/ScriptCapabilitiesConverter.cs
@@ -0,0 +1,32 @@
+// 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.Text.Json;
+using System.Text.Json.Serialization;
+
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Script.Json;
+
+/// Deserialize capabilities as either a JSON string value, or an array of JSON string values
+public class ScriptCapabilitiesConverter : JsonConverter {
+ public override bool HandleNull => true;
+
+ public ScriptCapabilitiesConverter() {}
+
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => reader.TokenType switch {
+ JsonTokenType.Null => string.Empty,
+ JsonTokenType.String => reader.GetString(),
+ JsonTokenType.StartArray => ReadCapsArray (ref reader, options),
+ _ => throw new JsonException(),
+ };
+
+ private static string ReadCapsArray(ref Utf8JsonReader reader, JsonSerializerOptions options) {
+ var elems = JsonSerializer.Deserialize(ref reader, options);
+ if (elems == null)
+ throw new JsonException();
+ return string.Join(' ', elems);
+ }
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/UpdateHandlerInfo.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/UpdateHandlerInfo.cs
new file mode 100644
index 00000000000000..a0b4c2b9ea9b74
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/UpdateHandlerInfo.cs
@@ -0,0 +1,18 @@
+// 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.Text.Json.Serialization;
+
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+public class UpdateHandlerInfo {
+
+ [JsonPropertyName("updatedTypes")]
+ public ImmutableArray UpdatedTypes {get; init;}
+
+ public UpdateHandlerInfo(ImmutableArray updatedTypes) {
+ UpdatedTypes = updatedTypes;
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Frontend.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Frontend.cs
new file mode 100644
index 00000000000000..c81c653da0c8ab
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Frontend.cs
@@ -0,0 +1,114 @@
+// 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.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Frontend;
+public static class Frontend
+{
+
+ public static int Main(string[] args)
+ {
+ if (!ParseArgs (args, out var config))
+ return 2;
+
+ return RunWithExitStatus(config).Result;
+
+ }
+
+ static async Task RunWithExitStatus(Microsoft.DotNet.HotReload.Utils.Generator.Config config)
+ {
+ try {
+ await Run(config);
+ return 0;
+ } catch (Microsoft.DotNet.HotReload.Utils.Generator.DiffyException exn) {
+ Console.Error.WriteLine ($"Error: {exn.Message}");
+ if (exn.ExitStatus == 0)
+ return 1; /* really shouldn't happen, but just in case */
+ return exn.ExitStatus;
+ }
+ }
+ static async Task Run (Microsoft.DotNet.HotReload.Utils.Generator.Config config)
+ {
+ var runner = Microsoft.DotNet.HotReload.Utils.Generator.Runner.Make (config);
+ await runner.Run ();
+ Console.WriteLine ("done");
+ }
+
+
+
+
+ private static void PrintUsage() {
+ Console.WriteLine("hotreload-delta-gen.exe -msbuild:project.csproj [-p:Key=Value ...] [-live|-script:script.json [-outputSummary:results.json]]");
+ }
+ static bool ParseArgs (string[] args, [NotNullWhen(true)] out Microsoft.DotNet.HotReload.Utils.Generator.Config? config)
+ {
+ // FIXME: not all these options make sense together
+ var builder = Microsoft.DotNet.HotReload.Utils.Generator.Config.Builder();
+
+ config = null;
+
+ for (int i = 0; i < args.Length; i++) {
+ const string msbuildOptPrefix = "-msbuild:";
+ const string scriptOptPrefix = "-script:";
+ const string outputSummaryPrefix = "-outputSummary:";
+ const string capabilitiesPrefix = "-capabilities:";
+ string fn = args [i];
+ if (fn.StartsWith(msbuildOptPrefix)) {
+ builder.ProjectPath = fn[msbuildOptPrefix.Length..];
+ } else if (fn == "-live") {
+ builder.Live = true;
+ } else if (fn.StartsWith("-p:")) {
+ var s = fn[3..];
+ if (s.IndexOf('=') is int j && j > 0 && j+1 < s.Length) {
+ var k = s[0..j];
+ var v = s[(j + 1)..];
+ // Console.WriteLine ($"got <{k}>=<{v}>");
+ builder.Properties.Add(KeyValuePair.Create(k,v));
+ } else {
+ PrintUsage ();
+ Console.WriteLine("\t-p option needs a key=value pair");
+ return false;
+ }
+ } else if (fn.StartsWith(scriptOptPrefix)) {
+ builder.ScriptPath = fn[scriptOptPrefix.Length..];
+ } else if (fn.StartsWith(outputSummaryPrefix)) {
+ builder.OutputSummaryPath = fn[outputSummaryPrefix.Length..];
+ } else if (fn.StartsWith(capabilitiesPrefix)) {
+ builder.EditAndContinueCapabilities.Add (fn[capabilitiesPrefix.Length..]);
+ } else {
+ PrintUsage();
+ Console.WriteLine ($"\tUnexpected trailing option {fn}");
+ return false;
+ }
+ }
+
+ if (String.IsNullOrEmpty(builder.ProjectPath)) {
+ PrintUsage();
+ Console.WriteLine ("\tmsbuild project is required");
+ return false;
+ }
+
+ if (!Xor(builder.Live, !String.IsNullOrEmpty(builder.ScriptPath))) {
+ PrintUsage();
+ Console.WriteLine("\tExactly one of -live or -script:script.json is required");
+ return false;
+ }
+
+ if (builder.Live && !String.IsNullOrEmpty(builder.OutputSummaryPath)) {
+ PrintUsage();
+ Console.WriteLine ("-outputSummary and -live cannot be used at the same time");
+ }
+
+ config = builder.Bake();
+ return true;
+ }
+
+ private static bool Xor (bool a, bool b) {
+ return !(a == b);
+ }
+
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Microsoft.DotNet.HotReload.Utils.Generator.Frontend.csproj b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Microsoft.DotNet.HotReload.Utils.Generator.Frontend.csproj
new file mode 100644
index 00000000000000..d7b0d005c8ac6b
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Microsoft.DotNet.HotReload.Utils.Generator.Frontend.csproj
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/HotReloadDeltaGeneratorComputeScriptOutputs.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/HotReloadDeltaGeneratorComputeScriptOutputs.cs
new file mode 100644
index 00000000000000..35e4c161cc8d3b
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/HotReloadDeltaGeneratorComputeScriptOutputs.cs
@@ -0,0 +1,142 @@
+// 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.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Text.Json;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Tasks;
+
+/// Given a DeltaScript, counts the number of elements and returns items for the .dmeta, .dil, and .dpdb
+/// files that the Generator would produce.
+public class HotReloadDeltaGeneratorComputeScriptOutputs : Microsoft.Build.Utilities.Task
+{
+ /// The name of the assembly produced by the current project
+ [Required]
+ public string BaseAssemblyName { get; set; }
+ /// The name of the json delta script
+ [Required]
+ public string DeltaScript {get; set; }
+
+
+ /// The generated delta outputs
+ /// Each item has a DeltaOutputType metadata with a value of "dmeta", "dil", "dpdb" or "updateHandlerJson"
+ /// indicating what kind of delta output it is.
+ [Output]
+ public ITaskItem[] DeltaOutputs { get; set; }
+
+ /// The (relative to the script file) sources that comprise the changes.
+ /// Each item has a DeltaForBaseline metadata that has the name of the baseline source file
+ [Output]
+ public ITaskItem[] DeltaSources { get; set; }
+
+ public HotReloadDeltaGeneratorComputeScriptOutputs()
+ {
+ BaseAssemblyName = string.Empty;
+ DeltaScript = string.Empty;
+ DeltaOutputs = Array.Empty();
+ DeltaSources = Array.Empty();
+ }
+
+ enum DeltaOutputType {
+ dmeta,
+ dil,
+ dpdb,
+ updateHandlerJson,
+ }
+
+ public override bool Execute()
+ {
+ if (!System.IO.File.Exists(DeltaScript))
+ {
+ Log.LogError("Hot reload delta script {0} does not exist", DeltaScript);
+ return false;
+ }
+ string baseAssemblyName = BaseAssemblyName;
+ Script.Json.Script? json;
+ try
+ {
+ json = Parse(DeltaScript).Result;
+ if (json?.Changes == null) {
+ Log.LogError("Hot reload delta script had no 'changes' array");
+ return false;
+ }
+ }
+ catch (JsonException exn)
+ {
+ Log.LogErrorFromException(exn, showStackTrace: true);
+ return false;
+ }
+
+ DeltaOutputs = ComputeOutputs (baseAssemblyName, json.Changes.Length);
+ DeltaSources = ComputeSources (json.Changes);
+ return true;
+ }
+
+ private static ITaskItem[] ComputeOutputs (string baseAssemblyName, int count)
+ {
+ const string deltaOutputTypeMetadata = "DeltaOutputType";
+ DeltaOutputType[] outputTypes = new DeltaOutputType[] {
+ DeltaOutputType.dmeta,
+ DeltaOutputType.dil,
+ DeltaOutputType.dpdb,
+ DeltaOutputType.updateHandlerJson,
+ };
+ int itemsPerRev = outputTypes.Length;
+ ITaskItem[] result = new TaskItem[itemsPerRev*count];
+ for (int i = 0; i < count; ++i)
+ {
+ int rev = 1+i;
+ foreach (var outputType in outputTypes)
+ {
+ int index = i*itemsPerRev + (int)outputType;
+ string name = NameForOutput(baseAssemblyName, rev, outputType);
+ result[index] = new TaskItem(name, new Dictionary { { deltaOutputTypeMetadata, outputType.ToString() } });
+ }
+ }
+ return result;
+ }
+
+ private static string NameForOutput(string baseName, int rev, DeltaOutputType t)
+ {
+ string ext = t switch {
+ DeltaOutputType.dmeta => "dmeta",
+ DeltaOutputType.dil => "dil",
+ DeltaOutputType.dpdb => "dpdb",
+ DeltaOutputType.updateHandlerJson => "handler.json",
+ _ => throw new Exception("unexpected")
+ };
+ return $"{baseName}.{rev}.{ext}";
+ }
+
+ private static ITaskItem[] ComputeSources (Script.Json.Change[] changes)
+ {
+ var count = changes.Length;
+ ITaskItem[] result = new TaskItem[changes.Length];
+ for (int i = 0; i < count; ++i)
+ {
+ // Just return the "update" documents. The baseline document should already be a item in the project
+ result[i] = new TaskItem(changes[i].Update, new Dictionary { { "DeltaForBaseline", changes[i].Document} });
+ }
+ return result;
+ }
+
+ public static async Task Parse(string scriptPath, CancellationToken ct = default)
+ {
+ using var stream = System.IO.File.OpenRead(scriptPath);
+ var options = new JsonSerializerOptions {
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
+ PropertyNameCaseInsensitive = true,
+ };
+ var json = await JsonSerializer.DeserializeAsync(stream, options: options, cancellationToken: ct);
+ return json;
+ }
+
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/Microsoft.DotNet.HotReload.Utils.Generator.Tasks.csproj b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/Microsoft.DotNet.HotReload.Utils.Generator.Tasks.csproj
new file mode 100644
index 00000000000000..0d657ca6aa97cf
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/Microsoft.DotNet.HotReload.Utils.Generator.Tasks.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs
new file mode 100644
index 00000000000000..d7648bdafad3e7
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+/// What we know about the base compilation
+///
+/// BaselineSolution: the solution we're working on
+/// BaselineProjectId: the project we're working on; FIXME: need to be more clever when there are project references
+/// BaselineOutputAsmPath: absolute path of the baseline assembly
+/// DocResolver: a map from document ids to documents
+/// ChangeMakerService: A stateful encapsulatio of the series of changes that have been made to the baseline
+internal record struct BaselineArtifacts (Solution BaselineSolution, ProjectId BaselineProjectId, string BaselineOutputAsmPath, DocResolver DocResolver, HotReloadService HotReloadService);
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs
new file mode 100644
index 00000000000000..1b4b528841fd0b
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs
@@ -0,0 +1,101 @@
+// 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.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.MSBuild;
+using Microsoft.Build.Framework;
+using System.Linq;
+using System.Collections.Immutable;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+internal record BaselineProject (Solution Solution, ProjectId ProjectId, HotReloadService HotReloadService) {
+
+ public static async Task Make (Config config, EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) {
+ (var changeMakerService, var solution, var projectId) = await PrepareMSBuildProject(config, capabilities, ct);
+ return new BaselineProject(solution, projectId, changeMakerService);
+ }
+
+ static async Task<(HotReloadService, Solution, ProjectId)> PrepareMSBuildProject (Config config, EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default)
+ {
+ // https://stackoverflow.com/questions/43386267/roslyn-project-configuration says I have to specify at least a Configuration property
+ // to get an output path, is that true?
+ var props = new Dictionary (config.Properties);
+ var workspace = MSBuildWorkspace.Create(props);
+ workspace.LoadMetadataForReferencedProjects = true;
+ _ = workspace.RegisterWorkspaceFailedHandler(diag => {
+ bool warning = diag.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning;
+ if (!warning)
+ Console.WriteLine ($"msbuild failed opening project {config.ProjectPath}");
+ Console.WriteLine ($"MSBuildWorkspace {diag.Diagnostic.Kind}: {diag.Diagnostic.Message}");
+ if (!warning)
+ throw new DiffyException ("failed workspace", 1);
+ });
+
+ ILogger? logger = null;
+#if false
+ logger = new Microsoft.Build.Logging.BinaryLogger () {
+ Parameters = "/tmp/enc.binlog"
+ };
+#endif
+ var project = await workspace.OpenProjectAsync (config.ProjectPath, logger, null, ct);
+
+ var service = new HotReloadService(
+ workspace.CurrentSolution.Services,
+ () => new([.. capabilities.ToString().Split(", ")]));
+
+ return (service, workspace.CurrentSolution, project.Id);
+ }
+
+
+ public async Task PrepareBaseline (CancellationToken ct = default) {
+ await HotReloadService.StartSessionAsync(Solution, ct);
+ var project = Solution.GetProject(ProjectId)!;
+
+ // gets a snapshot of the text of the baseline document in memory
+ // without this, roslyn doesn't appear to read the text until
+ // the document is really needed for the first time (when building a delta),
+ // at which point it may have already been changed on disk to a newer version.
+ var t = Task.Run (async () => {
+ foreach (var doc in project.Documents) {
+ await doc.GetTextAsync();
+ if (ct.IsCancellationRequested)
+ break;
+ }
+ }, ct);
+ if (!ConsumeBaseline (project, out string? outputAsm))
+ throw new Exception ("could not consume baseline");
+ var artifacts = new BaselineArtifacts() {
+ BaselineSolution = Solution,
+ BaselineProjectId = ProjectId,
+ BaselineOutputAsmPath = outputAsm,
+ DocResolver = new DocResolver (project),
+ HotReloadService = HotReloadService
+ };
+ await t;
+ return artifacts;
+
+ }
+
+ static bool ConsumeBaseline (Project project, [NotNullWhen(true)] out string? outputAsm)
+ {
+ outputAsm = project.OutputFilePath;
+ if (outputAsm == null) {
+ Console.Error.WriteLine ("msbuild project doesn't have an output path");
+ return false;
+ }
+ if (!File.Exists(outputAsm)) {
+ Console.Error.WriteLine ("msbuild project output assembly {0} doesn't exist. Build the project first", outputAsm);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Config.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Config.cs
new file mode 100644
index 00000000000000..634a2777db5c85
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Config.cs
@@ -0,0 +1,74 @@
+// 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.Generic;
+using System.IO;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+public class Config
+{
+
+ public static ConfigBuilder Builder () => new ();
+
+ public class ConfigBuilder {
+ internal ConfigBuilder () {}
+
+ public bool Live {get; set;} = false;
+
+ public List> Properties {get; set;} = new List> ();
+ public string ProjectPath {get; set; } = "";
+
+ public string ScriptPath {get; set; } = "";
+
+ public string OutputSummaryPath {get; set; } = "";
+ public Config Bake () {
+ return new MsbuildConfig(this);
+ }
+
+ public List EditAndContinueCapabilities {get; set; } = new List();
+
+ public bool NoWarnUnknownCapabilities {get; set;} = false;
+ }
+
+ protected Config (ConfigBuilder builder) {
+ Live = builder.Live;
+ Properties = builder.Properties;
+ ProjectPath = builder.ProjectPath;
+ ScriptPath = builder.ScriptPath;
+ OutputSummaryPath = builder.OutputSummaryPath;
+ EditAndContinueCapabilities = builder.EditAndContinueCapabilities.ToArray();
+ NoWarnUnknownCapabilities = builder.NoWarnUnknownCapabilities;
+ }
+
+ public bool Live { get; }
+
+ /// Additional properties for msbuild
+ public IReadOnlyList> Properties { get; }
+
+
+ /// the csproj for this project
+ public string ProjectPath { get; }
+
+ /// the files to watch for live changes
+ public string LiveCodingWatchPattern { get => "*.cs"; }
+
+ /// the directory to watch for live changes
+ public string LiveCodingWatchDir { get => Path.GetDirectoryName(ProjectPath) ?? "."; }
+
+ /// the path of a JSON script to drive the delta generation
+ public string ScriptPath { get; }
+
+ /// A path for a JSON file to collect the produced artifacts
+ public string OutputSummaryPath { get; }
+
+ /// A set of strings specifying edit and continue capabilities to pass to Roslyn
+ public string[] EditAndContinueCapabilities { get; }
+
+ /// If 'true' don't print a warning if a capability is not known.
+ public bool NoWarnUnknownCapabilities {get; }
+}
+
+internal class MsbuildConfig : Config {
+ internal MsbuildConfig (ConfigBuilder builder) : base (builder) {}
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Delta.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Delta.cs
new file mode 100644
index 00000000000000..bf78af22530662
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Delta.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+/// A Delta represents an input that is used to produce the metadata, IL and PDB emitted differences.
+/// It contains a Change which identifies the source document that changed and its updated contents
+public readonly record struct Delta (Plan.Change Change);
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaNaming.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaNaming.cs
new file mode 100644
index 00000000000000..3906c5a2a57838
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaNaming.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+/// Properties describing names of the artifacts for a given revision of a baseline assembly
+public class DeltaNaming {
+
+ readonly string _baseAssemblyPath;
+ readonly int _rev;
+
+ public DeltaNaming (string baseAssemblyPath, int rev) {
+ _rev = rev;
+ _baseAssemblyPath = baseAssemblyPath;
+ }
+ public string Dmeta => _baseAssemblyPath + "." + Rev + ".dmeta";
+ public string Dil => _baseAssemblyPath + "." + Rev + ".dil";
+ public string Dpdb => _baseAssemblyPath + "." + Rev + ".dpdb";
+
+ public string UpdateHandlerInfo => _baseAssemblyPath + "." + Rev + ".handler.json";
+ public int Rev => _rev;
+
+ public DeltaNaming Next()
+ {
+ return new DeltaNaming(_baseAssemblyPath, Rev+1);
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaOutputStreams.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaOutputStreams.cs
new file mode 100644
index 00000000000000..cfb3b301f00faf
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaOutputStreams.cs
@@ -0,0 +1,37 @@
+// 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.IO;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+public sealed class DeltaOutputStreams : IAsyncDisposable {
+ public Stream MetaStream {get; private set;}
+ public Stream IlStream {get; private set;}
+ public Stream PdbStream {get; private set;}
+
+ public Stream UpdateHandlerInfoStream {get; private set;}
+
+ public DeltaOutputStreams(Stream dmeta, Stream dil, Stream dpdb, Stream updateHandlerInfo) {
+ MetaStream = dmeta;
+ IlStream = dil;
+ PdbStream = dpdb;
+ UpdateHandlerInfoStream = updateHandlerInfo;
+ }
+
+ public void Dispose () {
+ MetaStream?.Dispose();
+ IlStream?.Dispose();
+ PdbStream?.Dispose();
+ UpdateHandlerInfoStream?.Dispose();
+ }
+
+ public async ValueTask DisposeAsync () {
+ if (MetaStream != null) await MetaStream.DisposeAsync();
+ if (IlStream != null) await IlStream.DisposeAsync();
+ if (PdbStream != null) await PdbStream.DisposeAsync();
+ if (UpdateHandlerInfoStream != null) await UpdateHandlerInfoStream.DisposeAsync();
+ }
+
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs
new file mode 100644
index 00000000000000..00809f649b8d56
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs
@@ -0,0 +1,130 @@
+// 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 BuildDelta (Delta delta, bool ignoreUnchanged = false,
+ Func? makeOutputs = default,
+ Action? 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);
+ }
+
+}
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DiffyException.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DiffyException.cs
new file mode 100644
index 00000000000000..be0ee11c01423a
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DiffyException.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+public class DiffyException : Exception {
+ public int ExitStatus { get; }
+
+ public DiffyException(int exitStatus) : base () {
+ ExitStatus = exitStatus;
+ }
+
+ public DiffyException (string message, int exitStatus) : base (message) {
+ ExitStatus = exitStatus;
+ }
+
+ public DiffyException (string message, Exception innerException, int exitStatus) : base (message, innerException) {
+ ExitStatus = exitStatus;
+ }
+}
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DocResolver.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DocResolver.cs
new file mode 100644
index 00000000000000..a5e2a9b5fa6828
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DocResolver.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+/// Maps a source file path to a DocumentId in a given Project
+public class DocResolver {
+
+ private readonly Project project;
+
+ private readonly ImmutableDictionary docMap;
+ public DocResolver(Project project) {
+ this.project = project;
+ this.docMap = BuildDocMap (project.Documents);
+ }
+
+ public Project Project { get => project; }
+
+ private static ImmutableDictionary BuildDocMap (IEnumerable docs)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+ foreach (var doc in docs) {
+ var key = doc.FilePath;
+ var value = doc.Id;
+ var kvp = KeyValuePair.Create(key!, value);
+ builder.Add(kvp);
+ }
+ return builder.ToImmutable();
+ }
+
+ public bool TryResolveDocumentId (string relativePath, [NotNullWhen(true)] out DocumentId id) {
+ var absolutePath = Path.GetFullPath(relativePath);
+ return docMap.TryGetValue(absolutePath, out id!);
+ }
+
+}
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EditAndContinueCapabilitiesParser.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EditAndContinueCapabilitiesParser.cs
new file mode 100644
index 00000000000000..137a3ccb4de397
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EditAndContinueCapabilitiesParser.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+public static class EditAndContinueCapabilitiesParser {
+
+ public readonly struct Token {
+ public readonly string Value {get; init;}
+ }
+ static public readonly Regex capabilitiesTokenizer = new (@"^\s*(?:(\S+)\s+)*(\S+)?$", RegexOptions.CultureInvariant);
+ public static IEnumerable Tokenize (string capabilities)
+ {
+ Match match = capabilitiesTokenizer.Match (capabilities);
+ if (!match.Success)
+ yield break;
+ foreach (Capture c in match.Groups[1].Captures) {
+ yield return new Token { Value = c.Value };
+ }
+ foreach (Capture c in match.Groups[2].Captures) {
+ yield return new Token { Value = c.Value };
+ }
+ }
+
+ public static IEnumerable Tokenize (IEnumerable capabilities)
+ {
+ foreach (var cap in capabilities) {
+ foreach (var token in Tokenize(cap))
+ yield return token;
+ }
+ }
+
+ internal static (IEnumerable capabilities, IEnumerable unknowns) Parse (IEnumerable tokens)
+ {
+ List unknowns = new();
+ List capabilities = new();
+ foreach (var tok in tokens)
+ {
+ if (ParseToken (tok, out var cap)) {
+ capabilities.Add (cap);
+ } else {
+ unknowns.Add (tok.Value);
+ }
+ }
+ return (capabilities, unknowns);
+ }
+
+ internal static (IEnumerable capabilities, IEnumerable unknowns) Parse (string capabilities)
+ {
+ return Parse(Tokenize(capabilities));
+ }
+
+ internal static (IEnumerable capabilities, IEnumerable unknowns) Parse (IEnumerable capabilities)
+ {
+ return Parse(Tokenize(capabilities));
+ }
+
+ internal static bool ParseToken (Token token, out EnC.EditAndContinueCapabilities res) =>
+ Enum.TryParse(token.Value, ignoreCase: true, out res);
+
+}
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EnC/EditAndContinueCapabilities.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EnC/EditAndContinueCapabilities.cs
new file mode 100644
index 00000000000000..3ce1e471cdc7fa
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EnC/EditAndContinueCapabilities.cs
@@ -0,0 +1,70 @@
+using System;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.EnC;
+
+/// Copied from https://github.com/dotnet/roslyn/blob/e8e6d30fe462edd48ce13f6438e91d26876c17bb/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilities.cs
+/// Keep in sync
+///
+/// The capabilities that the runtime has with respect to edit and continue
+///
+[Flags]
+public enum EditAndContinueCapabilities
+{
+ None = 0,
+
+ ///
+ /// Edit and continue is generally available with the set of capabilities that Mono 6, .NET Framework and .NET 5 have in common.
+ ///
+ Baseline = 1 << 0,
+
+ ///
+ /// Adding a static or instance method to an existing type.
+ ///
+ AddMethodToExistingType = 1 << 1,
+
+ ///
+ /// Adding a static field to an existing type.
+ ///
+ AddStaticFieldToExistingType = 1 << 2,
+
+ ///
+ /// Adding an instance field to an existing type.
+ ///
+ AddInstanceFieldToExistingType = 1 << 3,
+
+ ///
+ /// Creating a new type definition.
+ ///
+ NewTypeDefinition = 1 << 4,
+ ///
+ /// Adding, updating and deleting of custom attributes (as distinct from pseudo-custom attributes)
+ ///
+ ChangeCustomAttributes = 1 << 5,
+
+ ///
+ /// Whether the runtime supports updating the Param table, and hence related edits (eg parameter renames)
+ ///
+ UpdateParameters = 1 << 6,
+
+ ///
+ /// Adding a static or instance method, property or event to an existing type (without backing fields), such that the method and/or the type are generic.
+ ///
+ GenericAddMethodToExistingType = 1 << 7,
+
+ ///
+ /// Updating an existing static or instance method, property or event (without backing fields) that is generic and/or contained in a generic type.
+ ///
+ GenericUpdateMethod = 1 << 8,
+
+ ///
+ /// Adding a static or instance field to an existing generic type.
+ ///
+ GenericAddFieldToExistingType = 1 << 9,
+
+ ///
+ /// The runtime supports adding to InterfaceImpl table.
+ ///
+ AddExplicitInterfaceImplementation = 1 << 10,
+
+ AddFieldRva = 1 << 11,
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj
new file mode 100644
index 00000000000000..9cdcb16066e2be
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Plan/Change.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Plan/Change.cs
new file mode 100644
index 00000000000000..14445d6cf4005a
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Plan/Change.cs
@@ -0,0 +1,22 @@
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Plan;
+
+/// A plan is just a collection of changes
+/// where each change is some identification of the base document and
+/// some representation of the update.
+///
+/// For live coding, the collection will be an IAsyncEnumerable>,
+/// for a scripted plan it will be some parsed immutable list of changes.
+///
+/// Initially the changes are just Change, but then DocResolve will
+/// change it to a Chamge.
+public readonly record struct Change(TDoc Document, TUpdate Update);
+
+public static class Change {
+ public static Change Create(TDoc doc, TUpdate update) {
+ return new Change(doc, update);
+ }
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs
new file mode 100644
index 00000000000000..54368ece58747b
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs
@@ -0,0 +1,120 @@
+// 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.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator;
+
+public abstract class Runner {
+ readonly protected Config config;
+
+ protected Runner(Config config)
+ {
+ this.config = config;
+ }
+
+ public static Runner Make (Config config)
+ {
+ if (config.Live)
+ return new Runners.LiveRunner (config);
+ else
+ return new Runners.ScriptRunner (config);
+ }
+
+ public async Task Run (CancellationToken ct = default) {
+ await PrepareToRun(ct);
+ var capabilities = PrepareCapabilities();
+ var baselineArtifacts = await SetupBaseline (capabilities, ct);
+
+ var deltaProject = new DeltaProject (baselineArtifacts);
+ var derivedInputs = SetupDeltas (baselineArtifacts, ct);
+
+ await GenerateDeltas (deltaProject, derivedInputs, makeOutputs: MakeOutputs, outputsReady: OutputsReady, ct: ct);
+ // FIXME: do something for LiveRunner
+ if (OutputsDone != null)
+ await OutputsDone (ct);
+ }
+
+ /// Delegate that is called to create the delta output streams.
+ /// If not set, a default is used that writes the deltas to files.
+ protected Func? MakeOutputs {get; set; } = null;
+
+ /// Delegate that is called after the outputs have been emitted.
+ /// If not set, a default is used that does nothing.
+ protected Action? OutputsReady {get; set; } = null;
+
+ /// Called when all the outputs have been emitted.
+ protected Func? OutputsDone {get; set;} = null;
+
+ private async Task SetupBaseline (EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) {
+ BaselineProject baselineProject = await BaselineProject.Make (config, capabilities, ct);
+
+ var baselineArtifacts = await baselineProject.PrepareBaseline(ct);
+
+ Console.WriteLine ("baseline ready");
+ return baselineArtifacts;
+ }
+
+ /// Called just before we start generating deltas.
+ protected abstract Task PrepareToRun(CancellationToken ct = default);
+
+ /// Returns true if the runner has capabilities for the project, or false to use the config defaults.
+ protected abstract bool PrepareCapabilitiesCore (out EnC.EditAndContinueCapabilities capabilities);
+
+ protected EnC.EditAndContinueCapabilities PrepareCapabilities() {
+ EnC.EditAndContinueCapabilities configCaps = EnC.EditAndContinueCapabilities.None;
+ (var configuredCaps, var unknowns) = EditAndContinueCapabilitiesParser.Parse (config.EditAndContinueCapabilities);
+ foreach (var c in configuredCaps) {
+ configCaps |= c;
+ }
+ bool projectHasCaps = PrepareCapabilitiesCore (out var runnerCaps);
+ var totalCaps = configCaps | runnerCaps;
+ // If the project explicitly sets no capabilities, use None
+ if (totalCaps == EnC.EditAndContinueCapabilities.None && !projectHasCaps)
+ totalCaps = DefaultCapabilities ();
+ if (!config.NoWarnUnknownCapabilities) {
+ foreach (var unk in unknowns) {
+ Console.WriteLine ("Unknown EnC capability '{0}', ignored.", unk);
+ }
+ }
+ return totalCaps;
+ }
+
+ protected EnC.EditAndContinueCapabilities DefaultCapabilities ()
+ {
+ var allCaps = EnC.EditAndContinueCapabilities.Baseline
+ | EnC.EditAndContinueCapabilities.AddMethodToExistingType
+ | EnC.EditAndContinueCapabilities.AddStaticFieldToExistingType
+ | EnC.EditAndContinueCapabilities.AddInstanceFieldToExistingType
+ | EnC.EditAndContinueCapabilities.NewTypeDefinition
+ | EnC.EditAndContinueCapabilities.ChangeCustomAttributes
+ | EnC.EditAndContinueCapabilities.UpdateParameters
+ | EnC.EditAndContinueCapabilities.GenericAddMethodToExistingType
+ | EnC.EditAndContinueCapabilities.GenericUpdateMethod
+ | EnC.EditAndContinueCapabilities.GenericAddFieldToExistingType
+ | EnC.EditAndContinueCapabilities.AddExplicitInterfaceImplementation
+ | EnC.EditAndContinueCapabilities.AddFieldRva
+ ;
+ return allCaps;
+ }
+
+ private protected abstract IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default);
+
+ private async Task GenerateDeltas (DeltaProject deltaProject, IAsyncEnumerable deltas,
+ Func? makeOutputs = null,
+ Action? outputsReady = null,
+ CancellationToken ct = default)
+ {
+ await foreach (var delta in deltas.WithCancellation(ct)) {
+ Console.WriteLine ("got a change");
+ /* fixme: why does FSW sometimes queue up 2 events in quick succession after a single save? */
+ deltaProject = await deltaProject.BuildDelta (delta, ignoreUnchanged: config.Live, makeOutputs: makeOutputs, outputsReady: outputsReady, ct: ct);
+ }
+ }
+
+}
+
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs
new file mode 100644
index 00000000000000..256a582736b62a
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs
@@ -0,0 +1,57 @@
+// 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.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Runners;
+
+/// Generate deltas by watching for changes to the source files of the project
+internal class LiveRunner : Runner {
+ public LiveRunner (Config config) : base (config) { }
+
+ protected override Task PrepareToRun (CancellationToken ct = default) => Task.CompletedTask;
+
+ private protected override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default)
+ {
+ return Livecoding (baselineArtifacts, config.LiveCodingWatchDir, config.LiveCodingWatchPattern, ct);
+ }
+
+ protected override bool PrepareCapabilitiesCore (out EnC.EditAndContinueCapabilities caps) {
+ caps = EnC.EditAndContinueCapabilities.None;
+ return false;
+ }
+ private static async IAsyncEnumerable Livecoding (BaselineArtifacts baselineArtifacts, string watchDir, string pattern, [EnumeratorCancellation] CancellationToken cancellationToken= default) {
+ var last = DateTime.UtcNow;
+ var interval = TimeSpan.FromMilliseconds(250); /* FIXME: make this configurable */
+ var docResolver = baselineArtifacts.DocResolver;
+ var baselineProjectId = baselineArtifacts.BaselineProjectId;
+
+ using var fswgen = new Util.FSWGen (watchDir, pattern);
+ await foreach (var fsevent in fswgen.Watch(cancellationToken).ConfigureAwait(false)) {
+ if ((fsevent.ChangeType & WatcherChangeTypes.Changed) != 0) {
+ var e = DateTime.UtcNow;
+ Console.WriteLine($"change in {fsevent.FullPath} is a {fsevent.ChangeType} at {e}");
+ if (e - last < interval) {
+ Console.WriteLine($"too soon {e-last}");
+ continue;
+ }
+ Console.WriteLine($"more than 250ms since last change");
+ last = e;
+ var fp = fsevent.FullPath;
+ if (!docResolver.TryResolveDocumentId(fp, out var id)) {
+ Console.WriteLine ($"ignoring change in {fp} which is not in {baselineProjectId}");
+ continue;
+ }
+
+ yield return new Delta (Plan.Change.Create(id, fp));
+ }
+ }
+ }
+
+
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs
new file mode 100644
index 00000000000000..1f6a56b8e96968
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs
@@ -0,0 +1,97 @@
+// 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.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Runners;
+
+/// Generate deltas by reading a script from a configuration file
+/// listing the changed versions of the project source files.
+internal class ScriptRunner : Runner {
+
+ Script.ParsedScript? parsedScript;
+ public ScriptRunner (Config config) : base (config) {
+ if (!string.IsNullOrEmpty(config.OutputSummaryPath)) {
+ var writer = new JsonSummaryWriter(config.OutputSummaryPath);
+ OutputsReady = writer.OutputsReady;
+ OutputsDone = writer.OutputsDone;
+ }
+ }
+
+ private class JsonSummaryWriter {
+ private string OutputPath {get; }
+ private readonly List deltas;
+ public JsonSummaryWriter(string outputPath) {
+ OutputPath = outputPath;
+ deltas = new List();
+ }
+ internal void OutputsReady(DeltaNaming names, DeltaOutputStreams _streams) {
+ // FIXME: propagate the name of the updated assembly
+ deltas.Add(new OutputSummary.Delta("", names.Dmeta, names.Dil, names.Dpdb));
+ }
+
+ internal async Task OutputsDone(CancellationToken ct = default) {
+ using var s = File.OpenWrite(OutputPath);
+ var summary = new OutputSummary.OutputSummary(deltas.ToArray());
+ await System.Text.Json.JsonSerializer.SerializeAsync(s, summary, cancellationToken: ct);
+ }
+
+ }
+
+ protected override async Task PrepareToRun(CancellationToken ct = default)
+ {
+ var scriptPath = config.ScriptPath;
+ var parser = new Microsoft.DotNet.HotReload.Utils.Generator.Script.Json.Parser(scriptPath);
+ Script.ParsedScript parsed;
+ using (var scriptStream = new FileStream(scriptPath, FileMode.Open)) {
+ parsed = await parser.ReadAsync (scriptStream, ct);
+ }
+ parsedScript = parsed;
+ }
+
+ protected override bool PrepareCapabilitiesCore (out EnC.EditAndContinueCapabilities capabilities) {
+ capabilities = EnC.EditAndContinueCapabilities.None;
+ if (parsedScript == null || parsedScript.Capabilities == null)
+ return false;
+ capabilities = parsedScript.Capabilities.Value;
+ if (!config.NoWarnUnknownCapabilities) {
+ foreach (var unk in parsedScript.UnknownCapabilities) {
+ Console.WriteLine ($"Unknown EnC capability '{unk}' in '{config.ScriptPath}', ignored.");
+ }
+ }
+ return true;
+ }
+
+ private protected override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default)
+ {
+ if (parsedScript == null)
+ return Util.AsyncEnumerableExtras.Empty();
+ return ScriptedPlanInputs (parsedScript, baselineArtifacts, ct);
+ }
+
+ private static async IAsyncEnumerable ScriptedPlanInputs (Script.ParsedScript parsedScript, BaselineArtifacts baselineArtifacts, [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ await Task.CompletedTask; // to make compiler happy
+ var resolver = baselineArtifacts.DocResolver;
+ var artifacts = parsedScript.Changes.Select(c => new Delta(Plan.Change.Create(ResolveForScript(resolver, c.Document), c.Update)));
+ foreach (var a in artifacts) {
+ yield return a;
+ if (ct.IsCancellationRequested)
+ break;
+ }
+ }
+ private static DocumentId ResolveForScript (DocResolver resolver, string relativePath) {
+ if (resolver.TryResolveDocumentId(relativePath, out var id))
+ return id;
+ throw new DiffyException($"Could not find {relativePath} in {resolver.Project.Name}", exitStatus: 12);
+ }
+
+}
diff --git a/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Script/Json/Parsing.cs b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Script/Json/Parsing.cs
new file mode 100644
index 00000000000000..49e6ce72c19e06
--- /dev/null
+++ b/src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Script/Json/Parsing.cs
@@ -0,0 +1,65 @@
+// 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.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+using CancellationToken = System.Threading.CancellationToken;
+
+namespace Microsoft.DotNet.HotReload.Utils.Generator.Script.Json;
+
+/// Read a diff script from a json file
+public class Parser {
+
+ public readonly string Path;
+ private readonly string _absDir;
+ public Parser (string path) {
+ Path = path;
+ _absDir = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path))!;
+ }
+ public async ValueTask