Build Stack Review

One Command That Provisions Identical Toolchains for macOS, Linux, and WSL2 Without Dotfiles or Shell Scripts

Pinned manifests replace shell scripts for identical toolchains across platforms.

· 8 min read
Cover illustration for “One Command That Provisions Identical Toolchains for macOS, Linux, and WSL2 Without Dotfiles or Shell Scripts”
Reproducible Development Environments · October 28, 2026 · 8 min read · 1,907 words

One-command developer onboarding across macOS, Linux, and WSL2 requires a pinned, portable environment manifest. The manifest must define the toolchain, its system dependencies, and their locked inputs so that one command can resolve the appropriate artifacts on each platform. This gives the team a shared environment definition without project-level OS conditionals, with clear limits to parity where host dependencies sit outside the manifest.

Why the same toolchain produces different results on macOS, Linux, and WSL2

Three engineers clone the same repository on macOS, Linux, and WSL2. They install the specified toolchain, yet the project behaves differently on each machine. The repository has not changed between those installations. The underlying substrate has. macOS runs Darwin with Apple Clang and a BSD userland, Linux distributions vary in kernel version and system library defaults, and WSL2 runs a Microsoft-maintained Linux kernel fork based on upstream LTS releases with a small number of WSL-specific out-of-tree patches.

Those differences create distinct resolution paths before the software runs. A package version can match while the compiler or linked system library does not, so agreement at the language layer does not establish agreement across the environment. macOS adds another source of drift through Apple Command Line Tools changes between major OS releases. Those changes can change how the compiler behaves even when dependencies inside the development environment are pinned. The failure is architectural rather than accidental, because an environment definition cannot control a dependency it leaves to the host.

How conventional bootstrap tools fail at the layer below their reach

Shell bootstrap scripts express a sequence of operations. Their result depends on what was already installed when execution began. Two developers can run it on different days, or against different prior machine states, and receive materially different environments. More installation steps do not remove that dependence unless the resulting environment is fully accounted for.

Dotfiles have a narrower purpose, namely personal shell preferences and aliases. Their platform conditionals can handle differences, but someone has to maintain those branches by hand as platforms are added or OS versions change. Version managers and language lock files address other bounded parts of the stack. Pinning package versions does not also pin the runtime, its C libraries, or the compiler used for native extensions. Each tool assumes the host already has an environment it can work with.

These tools solve useful, smaller problems than cross-platform parity requires. All of them stop at the OS-level substrate beneath the settings, runtimes, and packages they manage. A replacement model must therefore change what gets defined as well as how installation commands are arranged.

What a declarative environment manifest actually captures that scripts cannot

A declarative manifest describes the desired environment as pinned, content-addressed inputs, including tools, runtimes, system libraries, and their interdependencies. Rather than replaying installation steps against an unknown machine state, the resolver constructs the environment from those declared inputs. The dependency graph becomes part of the definition. Deterministic resolution derives artifacts from that graph instead of accepting whichever compatible dependency happens to be present.

That distinction goes deeper than pinning “Node 22.” The manifest identifies a specific build artifact derived from specific inputs, including its C library and the compiler used to build it. Two builds with different transitive dependencies have different hashes and are different artifacts. A matching version label is therefore insufficient evidence of parity. The declared graph records distinctions that the label conceals.

Cross-platform resolution uses one shared definition to select appropriate artifacts for the host architecture and OS. It does not make Darwin and Linux identical. The resolver handles platform differences without per-OS conditionals in the project manifest, as long as the inputs can resolve on each target. Project environments can also coexist without global installation, activating for a working directory and exiting when the developer leaves. A Rails project using one Ruby version need not interfere with a Go CLI using a different toolchain.

How can a new engineer get the full toolchain with one command?

Consider a team with macOS laptops, Ubuntu CI, and one WSL2 engineer. Its developers need a common environment definition. The manifest declares every toolchain dependency, pins content-addressed inputs rather than mutable version strings, and evaluates on all three platforms without modification. The lock file it generates records exactly what resolved. That record gives a new laptop, a CI job, and a production build the same locked basis for constructing their environments.

The developer-facing workflow is then short because the definitional work is complete. A new engineer clones the repository and runs one command to get the full toolchain, without having to install a version manager separately or follow OS-specific README instructions. The command is an entry point whose reliability depends on the manifest assuming no undeclared toolchain dependency is already installed on the host.

Platform support still has workload-specific limits. WSL2 is a viable parity target for most API servers, web services, and scripted pipelines. GPU-intensive workloads face abstraction overhead, however, and CUDA toolkit versions are not equally supported. Teams with those workloads should validate on native Linux rather than treating successful environment resolution as proof of equivalent execution.

How the manifest becomes the foundation for supply chain security and SBOMs

The same declared input graph that supports reproducibility also supports auditability. Every input is known at definition time, including transitive C libraries and build-time tools. This changes the basis of the inventory. A Software Bill of Materials can be derived from the manifest’s graph instead of inferred solely by scanning a running artifact.

For example, a runtime scanner may miss a build tool that left traces in the output but is not itself present at runtime. The manifest records that tool as an input. The same principle extends coverage to dependencies such as nested native extensions and dynamically loaded shared objects that runtime scanners can overlook.

An environment hash can also connect a deployment to the exact manifest that produced it. When a CVE is disclosed, teams can query deployments for the affected input instead of rescanning every image to reconstruct that relationship. Provenance becomes a byproduct of the build. The security benefit is therefore not a separate feature bolted onto onboarding, but a consequence of using declared, identifiable inputs throughout the workflow.

How platform engineering teams use the manifest as a shared baseline across the organization

A versioned, named environment in a central registry gives platform teams a distribution mechanism for their standards. New developers pull the environment by name rather than translate setup documentation into local machine state. The baseline becomes an artifact that teams can use directly. Platform teams can publish a Python data science stack, a Go microservices stack, or a Node web stack, with developers extending those bases locally without modifying the canonical definition.

Version history also makes dependency changes reversible through a single rollback command against the manifest history. CI can reference the same manifest as local development. When both resolve from the same locked inputs, environment drift within that declared boundary no longer explains a local pass followed by a CI failure. This does not establish that every possible difference between local and CI execution has disappeared.

The organizational pattern extends beyond distributing packages. Rabobank’s platform engineering programme combined documented standards, golden paths, reusable infrastructure components, and automated onboarding and deployment. The time for new teams to get up to speed fell from months to weeks. That outcome illustrates the role of a shared path through the organization, without making the bank’s programme evidence for any particular manifest implementation.

AI agents as a new class of environment consumer that raises the stakes for parity

An AI agent acts on the environment state it observes. If that state differs from production, the agent can choose an action suited to the wrong baseline even when its reasoning follows the available evidence. Unlike a human developer noticing an unfamiliar tool version, an agent can turn the discrepancy into further actions, making the original source of error harder to attribute and correct.

The Model Context Protocol (MCP), an open protocol created by Anthropic, standardizes communication between agents and external tools or data sources through JSON-RPC messages. It separates the tool layer from the model layer so that compliant model clients can use filesystem, database, and API servers. That interoperability does not establish environment parity. An MCP server reports state from the environment in which it runs, whether or not that environment matches the intended target.

A reproducible, hermetically defined environment gives those reports the same baseline across model clients. It does not make every reported state correct by itself. MCP suits single-user and small-team workflows and is maturing toward large, multi-tenant production systems, but strict security and observability still depend heavily on implementation choices. Agents and developers still need to use the same manifest, pinned inputs, and verifiable state.

The real adoption cost and where the learning curve bites

Nix underpins the most complete implementations of this declarative model, but adopting it involves more than learning command syntax. Its functional language has a distinct evaluation model, and its documentation remains uneven despite improvement. Teams must learn a different way to describe system state. Flakes, which enable locked, reproducible evaluation, also remain flagged “experimental” in official documentation despite widespread production use, creating a credibility gap for adoption decisions.

The host boundary requires equal attention. Nix cannot pin the Apple Command Line Tools version, so a macOS system update can change how the compiler behaves in ways the manifest cannot control. Teams using macOS need to account for that version separately. This qualifies the promise of identical toolchains while leaving the declared environment model intact, because the unresolved variable is identifiable.

Containers raise a separate question about scope. Deployment isolation and the definition of environment inputs solve different problems, while local container isolation can separate developers from their tools and dotfiles. Human-readable manifest abstractions reduce the need to begin with raw Nix syntax without giving up the underlying reproducibility model. They make it easier to get started, though some maintenance work remains. The tradeoff is an upfront investment in explicit definitions against recurring drift costs whenever developers onboard or dependencies need to be rolled back.

How to move an existing team from bootstrap scripts to a declarative manifest without a flag-day migration

Teams can start the migration with one project’s development environment while their existing CI scripts and deployment pipelines keep running. A team in that position should first pin the compilers, runtimes, and CLI tools used daily, then extend coverage to system library dependencies. This makes consistent tool versions visible early. It also lets teams get a working environment before they understand the whole dependency graph, while making clear that deeper coverage is work still to be done.

The first parity test should be onboarding a new team member or contractor from the manifest alone, without consulting a README. Every step outside the one command exposes a gap to close. CI becomes the second test when it resolves from the same manifest as local development. Teams can track pipeline failure rates during the transition to see whether environment-drift failures go away, while allowing for other causes of failing builds.

Supply chain requirements can give the organization a reason to finish that coverage. A team required to produce an accurate SBOM can use the input graph as the driver for declaring dependencies that earlier scripts left implicit. The one-command promise is earned when setup no longer depends on instructions or toolchain inputs outside the shared definition.

More in Reproducible Development Environments