From d4e3330a8b9b6f237bc992ba7ee67b0b1a85ebe3 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Fri, 3 Apr 2026 14:34:41 -0400 Subject: [PATCH 1/4] Merge hotreload-utils source into runtime repo Copy the hotreload-utils source code from dotnet/hotreload-utils into src/tools/hotreload-delta-gen/ and replace the NuGet PackageReference with direct in-repo MSBuild targets, eliminating the cross-repo dependency. Changes: - Add 5 projects + shared code from hotreload-utils under src/tools/hotreload-delta-gen/ - Create eng/testing/hotreload-delta-gen.targets replacing the NuGet package's .targets file - Update 3 consumers (ApplyUpdate tests, WASM HotReload test, Mono sample) to import local targets instead of NuGet package - Add BuildTool and Tasks to src/libraries/pretest.proj for build ordering - Remove hotreload-utils dependency from eng/Version.Details.xml and eng/Version.Details.props - Suppress analyzers on imported code (RunAnalyzers=false) matching the illink pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Version.Details.props | 4 - eng/Version.Details.xml | 4 - eng/testing/hotreload-delta-gen.targets | 84 +++++++++++ .../tests/ApplyUpdate/Directory.Build.props | 11 -- .../tests/ApplyUpdate/Directory.Build.targets | 1 + src/libraries/pretest.proj | 4 + .../sample/mbr/console/ConsoleDelta.csproj | 13 +- .../ApplyUpdateReferencedAssembly.csproj | 12 +- .../Common/TempDirectory.cs | 28 ++++ .../hotreload-delta-gen/Directory.Build.props | 16 ++ .../Directory.Build.targets | 3 + ...HotReload.Utils.Generator.BuildTool.csproj | 12 ++ .../Program.cs | 4 + .../README.md | 31 ++++ ...tNet.HotReload.Utils.Generator.Data.csproj | 2 + .../OutputSummary/OutputSummary.cs | 42 ++++++ .../Script/Json/Script.cs | 30 ++++ .../Json/ScriptCapabilitiesConverter.cs | 32 ++++ .../UpdateHandlerInfo.cs | 18 +++ .../Frontend.cs | 114 ++++++++++++++ ....HotReload.Utils.Generator.Frontend.csproj | 7 + ...eloadDeltaGeneratorComputeScriptOutputs.cs | 142 ++++++++++++++++++ ...Net.HotReload.Utils.Generator.Tasks.csproj | 13 ++ .../BaselineArtifacts.cs | 16 ++ .../BaselineProject.cs | 101 +++++++++++++ .../Config.cs | 74 +++++++++ .../Delta.cs | 10 ++ .../DeltaNaming.cs | 27 ++++ .../DeltaOutputStreams.cs | 37 +++++ .../DeltaProject.cs | 130 ++++++++++++++++ .../DiffyException.cs | 23 +++ .../DocResolver.cs | 44 ++++++ .../EditAndContinueCapabilitiesParser.cs | 63 ++++++++ .../EnC/EditAndContinueCapabilities.cs | 70 +++++++++ ...ft.DotNet.HotReload.Utils.Generator.csproj | 18 +++ .../Plan/Change.cs | 22 +++ .../Runner.cs | 120 +++++++++++++++ .../Runners/LiveRunner.cs | 57 +++++++ .../Runners/ScriptRunner.cs | 97 ++++++++++++ .../Script/Json/Parsing.cs | 65 ++++++++ .../Script/ParsedScript.cs | 17 +++ .../Util/AsyncEnumerableExtras.cs | 9 ++ .../Util/FSWGen.cs | 79 ++++++++++ 43 files changed, 1669 insertions(+), 37 deletions(-) create mode 100644 eng/testing/hotreload-delta-gen.targets create mode 100644 src/tools/hotreload-delta-gen/Common/TempDirectory.cs create mode 100644 src/tools/hotreload-delta-gen/Directory.Build.props create mode 100644 src/tools/hotreload-delta-gen/Directory.Build.targets create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool.csproj create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/Program.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.BuildTool/README.md create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Microsoft.DotNet.HotReload.Utils.Generator.Data.csproj create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/OutputSummary/OutputSummary.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/Script.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/Script/Json/ScriptCapabilitiesConverter.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Data/UpdateHandlerInfo.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Frontend.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Frontend/Microsoft.DotNet.HotReload.Utils.Generator.Frontend.csproj create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/HotReloadDeltaGeneratorComputeScriptOutputs.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator.Tasks/Microsoft.DotNet.HotReload.Utils.Generator.Tasks.csproj create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Config.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Delta.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaNaming.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaOutputStreams.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DiffyException.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/DocResolver.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EditAndContinueCapabilitiesParser.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/EnC/EditAndContinueCapabilities.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Plan/Change.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Script/Json/Parsing.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Script/ParsedScript.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Util/AsyncEnumerableExtras.cs create mode 100644 src/tools/hotreload-delta-gen/Microsoft.DotNet.HotReload.Utils.Generator/Util/FSWGen.cs diff --git a/eng/Version.Details.props b/eng/Version.Details.props index c954ef976c7e75..1336b209584591 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.3.26176.106 11.0.0-preview.3.26176.106 11.0.0-preview.3.26176.106 - - 11.0.0-alpha.0.26173.1 11.0.0-alpha.1.26168.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 74e67f0c7962a8..5ff1aba71aa334 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 286ce291f642300fe36c66567620f65a526487af diff --git a/eng/testing/hotreload-delta-gen.targets b/eng/testing/hotreload-delta-gen.targets new file mode 100644 index 00000000000000..389d50f27d6f8a --- /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 8ec39373a5d998..85df49dc16acdf 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..ac576133284a62 --- /dev/null +++ b/src/tools/hotreload-delta-gen/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + $(NetCoreAppToolCurrent) + enable + Latest + tools\hotreload-delta-gen\ + + + 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..ad3b9205db0dc3 --- /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..6bdd9f2d66cea4 --- /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 ReadRawAsync (Stream stream, CancellationToken ct = default) { + var options = new JsonSerializerOptions { + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + }; + try { + var result = await JsonSerializer.DeserializeAsync