BinaryWrapper v2: Versioned CLI Contracts and Typed Output
BinaryWrapper connects versioned command-line interfaces to generated C# commands, typed environments and observable process execution. This series follows the implementation from help acquisition and build-time source generation to stdout/stderr parsing, versioned output contracts and consumer libraries.
This series documents the locally inspected implementation on 2 October 2026. “v2” names this architectural iteration; it does not assert a NuGet release numbered 2.0. Vagrant provides the main walkthrough; ten executable wrappers and a separate GitLab CI YAML schema consumer illustrate the shared mechanisms and their limits.
Start with a concrete execution
A generated command is a value you can inspect before running anything. Its environment is another typed value. The runner then turns process lines into observations with a producing channel, parser profile and version.
The following complete offline example references Vagrant, BinaryWrapper and BinaryWrapper.Testing. It feeds synthetic, source-shaped output to the real executor and Vagrant parser. It starts no VM.
using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.Vagrant;
var version = SemanticVersion.Parse("2.4.9");
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("vagrant"),
ExecutablePath = "vagrant",
DetectedVersion = version,
};
var command = new VagrantUpCommand(version)
{
MachineReadable = true,
Environment = new VagrantEnvironment { Home = "example/.vagrant.d" },
};
var transport = new FakeProcessRunner([
new OutputLine(
"1700000000,web,ui,output,Machine booted and ready!",
OutputSource.StdOut),
new OutputLine("provider diagnostic", OutputSource.StdErr),
]);
IVagrantRunner runner = new VagrantRunner(
new CommandExecutor(new DictionaryBinaryResolver(binding), transport),
binding.Identifier);
Console.WriteLine(string.Join(" | ", command.ToArguments()));
await foreach (var observation in runner.StreamAsync(command))
{
if (observation is VagrantMachineBooted ready)
Console.WriteLine($"Communicator ready: {ready.MachineName}");
if (observation.OutputLine is { } line)
Console.WriteLine($"{line.Source}: {line.Text}");
if (observation is VagrantCommandExited exited)
Console.WriteLine($"Process exit: {exited.ExitCode}");
}
var result = await runner.ExecuteAsync(command, new VagrantUpCollector());
Console.WriteLine(result.Match(
onSuccess: value => $"Ready machines: {value.MachinesReady.Count}",
onFailure: error => error.ToString()));using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.Vagrant;
var version = SemanticVersion.Parse("2.4.9");
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("vagrant"),
ExecutablePath = "vagrant",
DetectedVersion = version,
};
var command = new VagrantUpCommand(version)
{
MachineReadable = true,
Environment = new VagrantEnvironment { Home = "example/.vagrant.d" },
};
var transport = new FakeProcessRunner([
new OutputLine(
"1700000000,web,ui,output,Machine booted and ready!",
OutputSource.StdOut),
new OutputLine("provider diagnostic", OutputSource.StdErr),
]);
IVagrantRunner runner = new VagrantRunner(
new CommandExecutor(new DictionaryBinaryResolver(binding), transport),
binding.Identifier);
Console.WriteLine(string.Join(" | ", command.ToArguments()));
await foreach (var observation in runner.StreamAsync(command))
{
if (observation is VagrantMachineBooted ready)
Console.WriteLine($"Communicator ready: {ready.MachineName}");
if (observation.OutputLine is { } line)
Console.WriteLine($"{line.Source}: {line.Text}");
if (observation is VagrantCommandExited exited)
Console.WriteLine($"Process exit: {exited.ExitCode}");
}
var result = await runner.ExecuteAsync(command, new VagrantUpCollector());
Console.WriteLine(result.Match(
onSuccess: value => $"Ready machines: {value.MachinesReady.Count}",
onFailure: error => error.ToString()));The command emits --machine-readable and serializes Home as VAGRANT_HOME. A boot observation means that Vagrant's communicator readiness step reported completion. The zero process exit is a separate fact. The collector receives the same kinds of events in a second simulated execution, using a fresh parser.
Replace the transport with a real process runner only when you intend to execute an installed tool. Supplying DetectedVersion is metadata; it does not install that version or switch executables.
The system map
The architecture has three different times, each with its own inputs and failures.
The architecture chapter explains package ownership. Build-time generation distinguishes Roslyn-generated code from the maintenance-generated Vagrant message catalogue. Typed output follows both stdout and stderr through decoding.
Five reading paths
Choose a path according to the decision you need to make. These are ordered routes through shared chapters, with no separate versions of the technical explanation.
| Reader | Prerequisites and outcome | Route |
|---|---|---|
| Junior developer | Basic C# objects and async/await; construct a command and understand its observations. | 01 → 02 → 06 → 07 → 08 → 11 → Vagrant → 14 |
| Senior developer | C# APIs and testing; maintain a scraper, generator or parser without mixing their contracts. | 03 → 04 → 05 → 06 → 09 → 12 → 13 → Git and Packer |
| Architect | Dependency design; assess contract ownership, compatibility and application integration. | 02 → 05 → 09 → 10 → 11 → 12 → 14 |
| Product lead | CLI execution and output vocabulary; understand usable capabilities, evidence and coverage limits. | 01 → implementation matrix → output summary → versioning summary → 14 |
| Tech lead | Build and integration ownership; evaluate maintenance effort and a team's extension workflow. | 02 → 03 → 05 → 09 → 10 → 12 → 13 → implementation studies |
Chapters and concepts
Each chapter starts with its practical question, then develops the implementation and its limits.
| Chapter | Question it answers |
|---|---|
| 01 — Scope and value flow | What does BinaryWrapper expose, and which contracts connect its stages? |
| 02 — Architecture | Who owns acquisition, generated APIs, contracts and execution? |
| 03 — Versioned scraping | How do versions become reproducible help snapshots? |
| 04 — Parsing and replay | How can a help parser be repaired without reacquiring binaries? |
| 05 — Build-time generation | Which C# files are produced at compilation, and from which inputs? |
| 06 — Versioned commands | How are version availability and explicit argument values represented? |
| 07 — Process execution | How do both output streams and the actual exit status reach the consumer? |
| 08 — Typed output | How does a line become evidence-backed semantic observations? |
| 09 — Output versioning | Which parser is valid for this command, format and version? |
| 10 — Contracts | How can business code depend on messages without process implementations? |
| 11 — Environment generation | How do catalogues produce version-aware environment records? |
| 12 — Environment propagation | What do inheritance, removal and explicit empty values mean? |
| 13 — Extension and validation | How is a new integration introduced and maintained? |
| 14 — Consumer integration | How do commands, bindings, environments, parsers and results fit into an application? |
The implementation subtree contains one detailed study for each of the ten executable wrappers and a separate GitLab CI YAML schema-collection study.
Keep four compatibility questions separate
A wrapper can know how to construct a command while having no reviewed semantic parser for its output. Its environment can introduce another compatibility boundary.
| Evidence | What it establishes | What it leaves open |
|---|---|---|
| Cached help from a named version | The command surface observed during collection | Runtime messages, plugin output and unsampled releases |
| Generated command guards | Compatibility derived from sampled command trees | Whether the executable selected at runtime actually matches the supplied version |
| Reviewed output profile | The format and semantics the parser recognizes | Arbitrary hooks, application logs, translations and future protocols |
| Environment catalogue | Variable names, conversions and declared availability | Whether another flag or variable overrides their effect inside the tool |
The examples preserve raw evidence and explicitly separate observations from success. The resulting consumer API keeps invocation values, protocol assumptions and execution evidence inspectable.
Source snapshot
Source inspection: 2 October 2026. These implementation paths are relative to Net/FrenchExDev in the local ecosystem checkout. The v2 changes are largely uncommitted; these paths identify inspected code, without claiming that public branch links contain it.
BinaryWrapper/doc/ARCHITECTURE.mdBinaryWrapper/doc/CONSUMER-MIGRATION.md
Start with the scope · Explore implementations · Read the v1 article