Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 8 additions & 16 deletions src/SwaggerDiff.Tool/Commands/SnapshotCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);

Expand Down
83 changes: 59 additions & 24 deletions src/SwaggerDiff.Tool/Program.cs
Original file line number Diff line number Diff line change
@@ -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<SnapshotCommand>("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<ListCommand>("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<SnapshotInternalCommand>("_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<SnapshotCommand>("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<ListCommand>("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<SnapshotInternalCommand>("_snapshot")
.IsHidden();
});

return app.Run(args);
}
}