← Back to Novavero

The first package built by Nix natively on Windows

June 10, 2026 · Devon Tomlin · Novavero AI

$ nova-nix build pkgs\windows\hello.nix
  [build]  mingw-w64-x86_64-gcc-16.1.0-5-any.pkg.tar.zst
  ...      (17 fixed-output fetches, one per MSYS2 package)
  [build]  mingw-w64-seed
  [build]  hello
C:\nix\store\zr07i99kqnv48q29n706qxar7h1gfins-hello

$ C:\nix\store\zr07i99kqnv48q29n706qxar7h1gfins-hello\hello.exe
Hello from the first native Windows Nix build.

That's a C program compiled by a Nix derivation, natively on Windows. The compiler is a content-addressed store path. The source file is a store path. The output is a store path with a real NAR hash, registered in a SQLite database with scanned references. No WSL, no Cygwin, no Docker. cmd.exe spawned gcc.exe out of C:\nix\store because a derivation told it to.

As far as I can tell, this hasn't been shipped before. Native Windows support has been an open request since 2017, and the practical answer today is WSL. Serious effort came before this one. The nix-windows port carried the C++ codebase a long way before development wound down, and the difficulty of the problem is a big part of why I wanted to try.

Some context

nova-nix is a from-scratch Nix implementation that treats Windows as a first-class host. The evaluator is Haskell and the data layer is C99. I started it for game development: I wanted reproducible builds on the platform games are actually built on.

The claim "a from-scratch Nix" deserves skepticism, so here's the credential. nova-nix evaluates real nixpkgs, and (import <nixpkgs> {}).hello.drvPath, plus the entire 253-derivation closure behind it, hashes byte-for-byte identically to upstream nix-instantiate on the same nixpkgs tree. That's enforced by a CI job on every push, not measured once. Same parser semantics, same string-context rules, same ATerm serialization, same store-path derivation, same hashing, down to the byte. Evaluation parity shipped earlier; this post is about what came after: making builds real.

The bootstrap problem

On Linux, nixpkgs bootstraps from a bootstrapTools tarball: a seed of pre-built binaries that compiles everything else. Windows has no equivalent, because no Nix has ever run there. Designing it meant answering questions nobody had answered before.

Unpacking is a builtin. The seed toolchain arrives as MSYS2 .tar.zst packages. But nothing in the store can unpack them (the unpacker is the thing being bootstrapped), and Windows' ambient tar.exe is unpinned and can't be trusted with reproducibility. Upstream Nix bakes builtin:fetchurl into the binary for exactly this reason, so nova-nix follows the same logic one step further: builtin:unpack, an in-process extractor that a derivation can name as its builder. Hash-verified bytes in, store path out, no ambient tools.

The seed is one merged root. MSYS2 packages all unpack into a shared mingw64/ prefix. The seed derivation lists all 17 archives (the full runtime closure of mingw-w64-x86_64-gcc, each one a sha256-pinned fixed-output fetch), and builtin:unpack extracts them into a single output. The result is a working toolchain root at a single store path. GCC's relocatable prefix discovery does the rest: run gcc.exe -print-search-dirs from the store and every path it reports is inside its own store path.

Links become copies. Symlinks on Windows require Developer Mode or elevation, which a store output can't assume. The gcc package also contains hardlinks (g++.exe is a hardlink to c++.exe). Both are materialized as copies during extraction: deterministic everywhere, at a small size cost. RPATH has no Windows equivalent either. A subprocess resolves its DLLs via PATH, so the hello derivation puts the seed's bin on the build PATH. That one line is the Windows analogue of what stdenv's setup script does on Linux, and stage 1 will automate it.

cmd.exe is the bootstrap shell. There's no /bin/sh to point a builder at, and bash is one of the things the stdenv will build. So the proof derivation uses the one shell the OS guarantees: builder = "cmd.exe", which expands %out% and %src% from the build environment exactly the way /bin/sh expands $out on Linux. It's the only ambient tool in the chain, and it's temporary. Stage 1 rebuilds bash from source with the seed.

The whole thing is two Nix files. The seed is 17 pinned URLs and a derivation call; hello is a compiler invocation. They go through the same pipeline as anything else: eval writes the derivation closure, the builder topologically orders it, fixed-output fetches verify their hashes, and outputs land in the store NAR-hashed and registered.

What this is not, yet

Honesty section. Builds are unsandboxed, as Nix on macOS was for years (Windows has no usable chroot equivalent; Job Objects and AppContainers are future work). The seed pins rot upstream because MSYS2 prunes old package versions from its repo. The durable fix is mirroring the seed tarballs into our own binary cache, which is next. And this is stage 0 of a stdenv, not nixpkgs-on-Windows: there is no mkDerivation, no setup script, no cc-wrapper yet. Stage 1, rebuilding coreutils and bash from source and wrapping the compiler, is where the real grind starts.

But the hard claim stands. The Nix model (content-addressed storage, hash-verified inputs, derivation-driven builds, reproducible outputs) works natively on Windows. The chicken-and-egg is broken. Everything from here is engineering, not existence proof.

Standing on shoulders

None of this exists without Eelco Dolstra's thesis and twenty years of Nix, the nixpkgs maintainers whose bootstrap design this imitates, the people who tried Windows before me, and the MSYS2 and MinGW-w64 projects, whose packages literally are the stage-0 seed. nova-nix reimplements Nix's ideas faithfully enough to byte-match them. The ideas are theirs.

Try it

$ git clone https://github.com/Novavero-AI/nova-nix
$ cd nova-nix
$ cabal build
$ cabal run nova-nix -- build pkgs\windows\hello.nix
$ C:\nix\store\zr07i99kqnv48q29n706qxar7h1gfins-hello\hello.exe
Hello from the first native Windows Nix build.

Requires GHC 9.8+ on Windows. The store path you get should be exactly the one above. That's rather the point. We verified the stronger claim too: two independent cold bootstraps, from empty stores through fresh downloads to final link, produce byte-for-byte identical binaries (gcc needs -Wl,--no-insert-timestamp, or the linker writes the clock into 4 bytes of PE header; everything else was already deterministic).