From 7dd3429fc31c814a873af22fcecdd41253e00526 Mon Sep 17 00:00:00 2001 From: OluwaVader Date: Sun, 1 Mar 2026 01:13:46 +0100 Subject: [PATCH] avoid inlining running the app to let dependencies be loaded --- .../Commands/SnapshotCommand.cs | 24 ++---- src/SwaggerDiff.Tool/Program.cs | 83 +++++++++++++------ 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/SwaggerDiff.Tool/Commands/SnapshotCommand.cs b/src/SwaggerDiff.Tool/Commands/SnapshotCommand.cs index 0ba2488..9702143 100644 --- a/src/SwaggerDiff.Tool/Commands/SnapshotCommand.cs +++ b/src/SwaggerDiff.Tool/Commands/SnapshotCommand.cs @@ -314,11 +314,12 @@ private static int RunSnapshotSubprocess(string assemblyPath, string assemblyDir return 1; } - // Resolve tool paths for --additional-deps and --additionalprobingpath + // Resolve tool paths for subprocess invocation. + // We intentionally do NOT use --additional-deps because it causes eager resolution + // of all tool dependencies (Spectre.Console, etc.) which fails when they aren't in + // the target app's probing paths. Instead, Program.cs registers an AssemblyLoadContext + // resolver that lazily loads tool dependencies from the tool's own directory. var toolDll = typeof(SnapshotCommand).Assembly.Location; - var toolDir = Path.GetDirectoryName(toolDll)!; - var toolDepsFile = Path.Combine(toolDir, - Path.GetFileNameWithoutExtension(toolDll) + ".deps.json"); var nugetPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); @@ -327,23 +328,14 @@ private static int RunSnapshotSubprocess(string assemblyPath, string assemblyDir { "exec", "--depsfile", EscapePath(depsFile), - "--runtimeconfig", EscapePath(runtimeConfig) - }; - - if (File.Exists(toolDepsFile)) - args.AddRange(["--additional-deps", EscapePath(toolDepsFile)]); - - args.AddRange(["--additionalprobingpath", EscapePath(nugetPackages)]); - args.AddRange(["--additionalprobingpath", EscapePath(toolDir)]); - - args.AddRange( - [ + "--runtimeconfig", EscapePath(runtimeConfig), + "--additionalprobingpath", EscapePath(nugetPackages), EscapePath(toolDll), "_snapshot", "--assembly", EscapePath(assemblyPath), "--output", EscapePath(Path.GetFullPath(outputDir)), "--doc-name", docName - ]); + }; var processArgs = string.Join(" ", args); diff --git a/src/SwaggerDiff.Tool/Program.cs b/src/SwaggerDiff.Tool/Program.cs index 9ab80be..5f6124b 100644 --- a/src/SwaggerDiff.Tool/Program.cs +++ b/src/SwaggerDiff.Tool/Program.cs @@ -1,29 +1,64 @@ +using System.Runtime.CompilerServices; +using System.Runtime.Loader; using Spectre.Console.Cli; using SwaggerDiff.Tool.Commands; -var app = new CommandApp(); +namespace SwaggerDiff.Tool; -app.Configure(config => +public static class Program { - config.SetApplicationName("swaggerdiff"); - - config.AddCommand("snapshot") - .WithDescription("Generate OpenAPI snapshots from built assemblies. Auto-discovers ASP.NET Core web projects when run from a solution directory.") - .WithExample("snapshot") - .WithExample("snapshot", "--project", "./src/MyApi/MyApi.csproj") - .WithExample("snapshot", "--project", "./src/Api1/Api1.csproj", "--project", "./src/Api2/Api2.csproj") - .WithExample("snapshot", "--exclude", "MyApi.Tests", "--exclude-dir", "tests") - .WithExample("snapshot", "-c", "Release", "--output", "Docs/Versions") - .WithExample("snapshot", "--assembly", "./bin/Release/net8.0/MyApi.dll"); - - config.AddCommand("list") - .WithDescription("List available OpenAPI snapshots in a directory.") - .WithExample("list") - .WithExample("list", "--dir", "Docs/Versions"); - - // Internal command used by the two-stage subprocess pattern — hidden from help - config.AddCommand("_snapshot") - .IsHidden(); -}); - -return app.Run(args); + public static int Main(string[] args) + { + // This is critical for the Stage 2 subprocess pattern: when the tool DLL is loaded via + // `dotnet exec` with the target app's deps.json, the runtime won't know about tool-specific + // dependencies. The resolver loads them from the tool's own directory. + RegisterToolAssemblyResolver(); + return RunApp(args); + } + + private static void RegisterToolAssemblyResolver() + { + var toolDir = Path.GetDirectoryName(typeof(Program).Assembly.Location)!; + + AssemblyLoadContext.Default.Resolving += (context, assemblyName) => + { + var candidatePath = Path.Combine(toolDir, assemblyName.Name + ".dll"); + return File.Exists(candidatePath) ? context.LoadFromAssemblyPath(candidatePath) : null; + }; + } + + // NoInlining prevents the JIT from inlining this into Main, which would cause it to + // try to resolve Spectre.Console types before the assembly resolver is registered. + [MethodImpl(MethodImplOptions.NoInlining)] + private static int RunApp(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + config.SetApplicationName("swaggerdiff"); + + config.AddCommand("snapshot") + .WithDescription( + "Generate OpenAPI snapshots from built assemblies. Auto-discovers ASP.NET Core web projects when run from a solution directory.") + .WithExample("snapshot") + .WithExample("snapshot", "--project", "./src/MyApi/MyApi.csproj") + .WithExample("snapshot", "--project", "./src/Api1/Api1.csproj", "--project", + "./src/Api2/Api2.csproj") + .WithExample("snapshot", "--exclude", "MyApi.Tests", "--exclude-dir", "tests") + .WithExample("snapshot", "-c", "Release", "--output", "Docs/Versions") + .WithExample("snapshot", "--assembly", "./bin/Release/net8.0/MyApi.dll"); + + config.AddCommand("list") + .WithDescription("List available OpenAPI snapshots in a directory.") + .WithExample("list") + .WithExample("list", "--dir", "Docs/Versions"); + + // Internal command used by the two-stage subprocess pattern — hidden from help + config.AddCommand("_snapshot") + .IsHidden(); + }); + + return app.Run(args); + } +}