Nix (flakes) (Polyglot glue)
A purely-functional, content-addressed build/package system whose flake layer turns any directory with a flake.nix into a hermetic, lock-pinned, composable unit — making Nix the polyglot "glue" that pins, fetches, and wires together multi-language monorepos that individual ecosystem tools (cargo, npm, dub) can only see one slice of.
| Field | Value |
|---|---|
| Language | C++ (evaluator/store, ~C++23); the Nix expression language for flake manifests |
| License | LGPL-2.1-or-later |
| Repository | NixOS/nix |
| Documentation | Nix Reference Manual — nix flake · nix.dev flakes guide |
| Category | Polyglot Glue |
| Workspace model | Virtual graph of flakes: each flake.nix is a node; inputs are typed edges; a flake.lock pins the DAG |
| First released | Flakes: Nix 2.4 (Nov 2021, RFC 49); still gated behind the flakes experimental feature |
| Latest release | Nix 2.34.7 (2026-05-04); local checkout analysed here is 2.35.0-dev on master |
Latest release: upstream
Nix 2.34.7(2026-05-04). Flakes are still formally an experimental feature —flakesmust be enabled innix.confor via--extra-experimental-features 'nix-command flakes'. The notable forks Lix (2.94.0) and Determinate Nix (3.13.x, tracking upstream2.32.4) ship the same flake schema (lock-fileversion 7). All source citations below are to the upstreammastertree (Nix 2.35.0-dev, commit41b28ad2e) atsrc/libflake/.
Overview
What it solves
Every language ecosystem ships its own package manager — cargo for Rust, npm for JS, pip/uv for Python, dub for D — and each is blind to the others. A real monorepo is usually polyglot: a Rust service, a TypeScript frontend, a D CLI, plus the C libraries and system toolchains they all link against. No single language tool can declare, pin, and reproducibly fetch that whole graph. Nix flakes do.
A flake is, per the manual, "a filesystem tree (typically fetched from a Git repository or a tarball) that contains a file named flake.nix in the root directory" (src/nix/flake.md). flake.nix declares two things: inputs (typed references to other flakes or raw source trees — pinned by content hash) and outputs (a pure function from the realised inputs to an attribute set of packages, dev shells, checks, NixOS modules, apps, …). Because inputs are content-addressed and recorded in a machine-generated flake.lock, the entire transitive dependency graph is reproducible bit-for-bit across machines, with no ambient state.
Within this catalog Nix occupies a different layer than the language-native tools. cargo resolves a Rust crate graph; go-work stitches Go modules; pnpm hoists a JS store. Nix sits above all of them: a flake input can be a nixpkgs snapshot, a pinned C library, a sibling flake in the same repo, or a non-flake tarball, and the outputs function can invoke any of those language tools inside a hermetic sandbox. It is the closest thing the field has to a universal, content-addressed lockfile + task graph for arbitrary languages — at the cost of requiring everything to be expressed in the Nix language. Compare its all-encompassing-engine ambition with bazel and buck2 (which also model a polyglot, hermetic, cached build DAG, but with their own non-purely-functional rule languages).
Design philosophy
Flakes formalise three older Nix conventions — pinning inputs, a standard output schema, and a discoverable entry point — into one composable unit. The manual's own framing (src/nix/flake.md):
"Flakes are the unit for packaging Nix code in a reproducible and discoverable way. They can have dependencies on other flakes, making it possible to have multi-repository Nix projects."
Three consequences follow, and they shape the whole model:
Inputs are pinned, not ranged. Unlike SemVer constraint solvers (
cargo,npm), a flake input resolves to an exact locked revision plus anarHashof the fetched tree. There is no version solver and no "compatible range" — "Inputs specified inflake.nixare typically 'unlocked' … To ensure reproducibility, Nix will automatically generate and use a lock file calledflake.lock" (flake.md). The lock is a graph, not a flat list.outputsis a pure function. It receives the realised inputs (each with anoutPathin the content-addressed store) and returns data — derivations, modules, shells. Evaluation is lazy and side-effect-free; the only impurity (fetching) is pushed to the edges and recorded in the lock.One standard output schema → discoverability.
packages.<system>.<name>,devShells.<system>.<name>,checks.<system>.<name>,apps.<system>.<name>(src/nix/flake-check.md) give the CLI a uniform place to look, sonix build .#foo,nix run,nix develop, andnix flake checkwork on any flake without per-project configuration.
Nix is the polyglot data point in this survey the way bazel is the polyglot orchestrator: both subordinate every language's native tool to a single hermetic graph. For the D-specific stakes of all this, see the D async/build landscape; the dub baseline notes that Sparkles itself is already built with a Nix flake.
Core concepts and types
| Concept | Type / file | Role |
|---|---|---|
| Flake reference | FlakeRef (flakeref.cc) | A typed, parseable locator: github:, git+, path:, tarball:, … |
| Indirect ref | type = "indirect" | Symbolic name resolved through the flake registry |
| Parsed flake | Flake (flake.hh) | originalRef → resolvedRef → lockedRef, plus inputs, config |
| Parsed input | FlakeInput (flake.hh) | One inputs.<id> entry: a ref, an isFlake flag, follows, overrides |
| Lock graph | LockFile / Node (lockfile.cc) | The DAG written to flake.lock; version 7 |
| Locked whole flake | LockedFlake (flake.hh) | Flake + LockFile + per-node SourcePaths + a Fingerprint |
| Lock options | LockFlags (flake.hh) | recreateLockFile, updateLockFile, inputOverrides, inputUpdates, … |
| Lock algorithm | lockFlake → computeLocks (flake.cc) | Recursive topological resolution of the input graph |
| Output evaluator | callFlake + call-flake.nix | Lazily ties locked nodes into the self/inputs arguments of outputs |
| Eval cache key | Fingerprint (Hash) (flake.cc) | Memoises evaluation per locked-flake + subdir + lock-file hash |
| CLI installable | InstallableFlake (installable-flake.cc) | flakeref#attrpath → an output attribute to build/run |
How it works
A flake manifest
flake.nix is an attribute set with description, inputs, and outputs (flake.md):
{
description = "A flake for building Hello World";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-20.03";
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default =
with import nixpkgs { system = "x86_64-linux"; };
stdenv.mkDerivation {
name = "hello";
src = self;
buildPhase = "gcc -o hello ./hello.c";
installPhase = "mkdir -p $out/bin; install -t $out/bin hello";
};
};
}outputs is a function: its argument set is "the outputs of each of the input flakes keyed by their identifier", plus the special self input that refers to this flake's own outputs and source tree (flake.md). The schema of the returned set is conventional — nix flake check enforces that packages.<system>.<name>, devShells.<system>.<name>, checks.<system>.<name> and friends are derivations of the right kind (flake-check.md).
Flake references: the typed locator
Every input and every CLI target is a FlakeRef, with both an attribute-set form and a URL-like shorthand. The type discriminator drives a corresponding fetcher (flake.md, flakeref.cc):
type | URL-like example | Fetched as |
|---|---|---|
indirect | nixpkgs, nixpkgs/nixos-unstable | registry lookup → another ref |
path | path:../lib, ./sub/dir | a local directory (no history) |
git | git+https://example.org/repo?ref=main&rev=… | a Git working tree / remote |
github | github:NixOS/nixpkgs/<rev-or-ref> | a GitHub tarball (no full history) |
gitlab / sourcehut | gitlab:veloren/veloren | host-specific tarball |
tarball / file | https://…/archive.tar.gz | a content tree, hashed by narHash |
The github: fetcher is the canonical optimisation: "These are downloaded as tarball archives, rather than through Git. This is often much faster and uses less disk space since it doesn't require fetching the entire history" (flake.md). The relative path:/./ form is what lets one repo's flake depend on a sibling flake in the same tree — the monorepo cross-ref primitive (see Workspace declaration).
The lock file: a graph isomorphic to the dependency graph
flake.lock is JSON, currently version 7 (lockfile.cc, where json["version"] = 7 and the reader rejects anything < 5 or > 7). It is, per the manual, "a graph structure isomorphic to the graph of dependencies of the root flake. Each node in the graph (except the root node) maps the (usually) unlocked input specifications in flake.nix to locked input specifications" (flake.md):
{
"version": 7,
"root": "n1",
"nodes": {
"n1": { "inputs": { "nixpkgs": "n2", "grcov": "n4" } },
"n2": {
"inputs": {},
"locked": {
"owner": "edolstra",
"repo": "nixpkgs",
"rev": "7f8d4b088e2df7fdb6b513bc2d6941f1d422a013",
"type": "github",
"lastModified": 1580555482,
"narHash": "sha256-OnpEWzNxF/AU4KlqBXM2s5PWvfI5/BS6xQrPvkF5tO8="
},
"original": { "id": "nixpkgs", "type": "indirect" }
}
}
}Each non-root node carries three keys: original (the unlocked spec from flake.nix), locked (the resolved fetchTree args including rev and narHash), and inputs (outgoing edges to other node labels). The graph form is deliberate — it lets two flakes reference each other (a cycle), and it de-duplicates shared transitive inputs into a single node. The root node omits original/locked "because we cannot record the commit hash or content hash of the root flake, since modifying flake.lock will invalidate these" (flake.md).
NOTE
narHash is what makes the lock substitutable from a binary cache: it lets Nix compute the store path of an input tree without re-fetching, and the other recorded attributes (lastModified, revCount) supply data not encoded in the store path. See Caching.
Resolving the lock: computeLocks
lockFlake (flake.cc) drives lock generation through a single recursive closure, computeLocks, that walks the input graph node-by-node. Its signature captures the algorithm: it carries the inputs of the current node, the target lock Node, the path of this node in the lock graph, the old node (so unchanged locks can be copied verbatim rather than re-fetched), and a followsPrefix for resolving follows:
// src/libflake/flake.cc — the recursive locker (signature, abridged)
std::function<void(
const FlakeInputs & flakeInputs,
ref<Node> node,
const InputAttrPath & inputAttrPathPrefix,
std::shared_ptr<const Node> oldNode, // copy locks from here if unchanged
const InputAttrPath & followsPrefix,
const SourcePath & sourcePath,
bool trustLock)>
computeLocks;For each input the closure: (1) collects ancestor overrides (inputs.nixops.inputs.nixpkgs.url = …); (2) defers follows (a symbolic edge to another node — it "may refer to an input path we haven't processed yet"); (3) if there is no ref, synthesises an indirect ref to be resolved in the registry; and (4) otherwise fetches/resolves the input, recursing into itsflake.nix inputs. Crucially, if the old lock already pins this input and the spec is unchanged, the existing lock is reused, so nix build on an unchanged tree fetches nothing.
Evaluating outputs: call-flake.nix
Once the lock graph exists, callFlake hands it to a small Nix bootstrap script, call-flake.nix, which lazily reconstructs the self/inputs arguments. It is the heart of the model and worth reading in full; the recursive core:
# src/libflake/call-flake.nix (abridged)
allNodes = mapAttrs (
key: node:
let
sourceInfo = if hasOverride then overrides.${key}.sourceInfo
else if isRelative then parentNode.sourceInfo # path: sibling
else fetchTreeFinal (node.info or {} // removeAttrs node.locked ["dir"]);
flake = import (outPath + "/flake.nix");
# Tie each input id to the already-resolved result of its target node:
inputs = mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}.result)
(node.inputs or {});
# Call this flake's own outputs, threading `self`:
outputs = flake.outputs (inputs // { self = result; });
result = outputs // sourceInfo // { inherit outPath inputs outputs sourceInfo; _type = "flake"; };
in { inherit result outPath sourceInfo; }
) lockFile.nodes;
in allNodes.${lockFile.root}.resultThree things stand out. First, the whole thing is one mapAttrs over the lock graph with a let-bound result referenced inside inputs — Nix's laziness turns the static graph into demand-driven topological evaluation: a node's outputs is forced only when something pulls on it, and self = result closes the self-reference. Second, resolveInput chases follows edges (["dwarffs" "nixpkgs"]) back through the root. Third, isRelative path: inputs reuse the parent's sourceInfo and append a subpath — that is exactly how a sibling flake in the same repo is wired without re-fetching anything.
CLI resolution: flakeref#attrpath
On the command line a target is <flakeref>#<attrpath>. InstallableFlake (installable-flake.cc) locks the flake, opens its eval cache, and walks the attribute path; if the fragment is empty it tries a list of default prefixes (installables.cc):
// src/libcmd/installables.cc — default attr paths / prefixes (abridged)
Strings getDefaultFlakeAttrPaths() // bare `nix build .`
{ return {"packages." + thisSystem + ".default", "defaultPackage." + thisSystem}; }
Strings getDefaultFlakeAttrPathPrefixes() // `nix build .#hello`
{ return { "packages." + thisSystem + ".", /* … */ }; }So nix build .#hello becomes packages.<system>.hello, and a bare nix build resolves packages.<system>.default — the uniform schema doing the navigation.
Workspace declaration & topology
Nix has no [workspace] block, no members glob, and no monorepo concept in the manifest. Topology is expressed entirely through the inputs graph: a monorepo is modelled as one root flake whose inputs point at the constituent flakes — including sibling directories in the same repository via relative path: (or git+file:) references.
# Root flake.nix of a polyglot monorepo (illustrative)
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
# Sibling flakes in the same tree — the cross-ref primitive:
core-cli.url = "path:./libs/core-cli";
backend.url = "path:./apps/backend";
backend.inputs.nixpkgs.follows = "nixpkgs"; # dedupe transitive nixpkgs
};
outputs = { self, nixpkgs, core-cli, backend }: { /* compose them */ };
}Discovery semantics (flake.md):
- No globbing. Members are enumerated explicitly under
inputs; there is nomembers = ["libs/*"]array as incargoorpnpm. (Some community frameworks —flake-parts,flake-utils— add ergonomic module/eachSystemsugar on top, but the core has no native member glob.) - Relative
path:is the local cross-ref. "aflake.nixin the root of a tree can usepath:./footo access the flake in subdirectoryfoo, butpath:../baris illegal" — relative path inputs must stay inside the same tree, and a relative-path node reuses the parent's fetchedsourceInfo(call-flake.nix). - Upward
flake.nixsearch. Given a path with noflake.nix, Nix searches upward "until it finds … the Git repository root, or the filesystem root, or a folder on a different mount point" — so.resolves to the enclosing flake. - Sub-directory flakes. The
dir=parameter "enables having multiple flakes in a repository or tarball"; thesubdiris appended to the fetchedoutPath.
IMPORTANT
This is a fundamentally different topology model than the rest of the catalog. Where go-work, cargo and pnpm have a dedicated root manifest that lists members, Nix has a single uniform node type (the flake) recursively composed by typed references. Every member is a first-class flake in its own right — there is no privileged "virtual root."
Dependency handling & isolation
Isolation in Nix is total and predates flakes: every build output lives at a content-addressed store path /nix/store/<hash>-<name>, where the hash derives from all build inputs. There is no hoisting, no symlink tree to de-duplicate, and no shared mutable node_modules/target — two flakes that depend on different nixpkgs revisions get two distinct store paths and never collide. This is a stronger isolation guarantee than the virtual-store / hoisting schemes of the JS world.
Flake-level dependency handling adds three operators on top of the input graph (flake.md):
follows— deduplication.inputs.nixops.inputs.nixpkgs.follows = "nixpkgs"rewrites a transitive input to point at an existing node, collapsing N copies of nixpkgs into one. "The value of thefollowsattribute is a/-separated sequence of input names denoting the path of inputs to be followed from the root flake." This is the flake analogue of a workspace catalog/inheritance — one pinnednixpkgs, referenced everywhere.- Transitive override.
inputs.nixops.inputs.nixpkgs.url = …replaces a dependency's dependency outright.computeLocksrecords these in anoverridesmap keyed by the full input attr-path (flake.cc). - Non-flake inputs.
inputs.grcov = { …; flake = false; }pins a raw source tree (noflake.nix) as a content-addressed input — how a Nix flake pins a plain C library, a vendored blob, or another language's repo.
Cross-workspace local references are the relative path: inputs described above: a member flake is depended on by store-path identity, not by publishing to a registry. The lock pins each by narHash, so the local graph is as reproducible as the remote one.
Task orchestration & scheduling
Flakes do not have a task pipeline in the turborepo/nx sense (named tasks with dependsOn edges). Instead, the task DAG is the derivation graph, one layer below flakes, and it is the oldest part of Nix:
- The DAG. Every
mkDerivation(and thus every package, dev shell, orchecks.<system>.<name>) is a node; itsinputDrvsare edges. Nix computes this graph by lazily evaluating the flakeoutputs, then realises it. - Change detection by input hashing. A derivation's store path is a hash of all its inputs (sources, dependencies, builder, env). If nothing in that closure changed, the output already exists in the store and is not rebuilt — this is content-addressed change detection, equivalent in spirit to
turborepo's input hashing but applied to the whole build graph, not just declared task inputs. - Concurrent execution. The builder schedules independent derivations in parallel up to
--max-jobs N(local) and--cores N(per-build), and can offload to remote builders (--builders) — a distributed build scheduler. nix flake checkas the aggregate task. It evaluates the flake and builds everychecks.<system>.<name>derivation (flake-check.md) — the closest flake-level analogue to "run all the monorepo's tests," with parallel per-check scheduling and--keep-goingto collect all failures.
What flakes lack relative to dedicated orchestrators: there is no notion of affected-since-a-git-ref selection at the flake layer (you express that by which derivations you ask to build), and no per-task input/output declaration distinct from the derivation graph. The trade is that Nix's change detection is sound by construction (it hashes the real closure), where a turbo/nx task graph relies on the user declaring inputs/outputs correctly.
Caching & remote execution
This is Nix's deepest strength, and it operates at two layers:
The content-addressed store as a local cache. A realised derivation is keyed by the hash of its full input closure; rebuilding is a store lookup. The flake evaluation layer is cached separately by a
Fingerprint(flake.cc):cpp// src/libflake/flake.cc — LockedFlake::getFingerprint (abridged) auto fingerprint = flake.lockedRef.input.getFingerprint(store); *fingerprint += fmt(";%s;%s", flake.lockedRef.subdir, lockFile); // … plus revCount / lastModified when they affect evaluationBecause the fingerprint folds in the locked ref, the subdir, and the full lock file, an unchanged locked flake reuses its cached evaluation — so repeated
nix build/nix flake showon a pinned tree skip both fetching and re-evaluation.Binary caches as remote build caches. Any store path can be pushed to / pulled from a binary cache (
cache.nixos.org, an S3 bucket, a self-hosted Attic/Cachix, or any HTTP store). Before building, Nix queries substituters and downloads the prebuilt output if its hash is present — exactly the remote-cache hit thatturborepoandbazelprovide, but for arbitrary builds and shared across an entire organisation or the public. The flake lock'snarHashis what lets an input tree also be substituted from a cache without re-fetching (flake.md).Remote / distributed execution. Nix's
--buildersmechanism farms individual derivations out to remote machines (including cross-architecture builders), and the Hydra CI system is a flake-aware distributed build farm. This is Nix's own protocol rather than the Bazel REAPI thatbuildbarn/buildbuddy/nativelinkimplement — Nix predates and sits orthogonal to REAPI — but it delivers the same remote-execution + shared-cache outcome.
NOTE
The crucial difference from a per-language cache: Nix's cache key is the entire build closure including the C toolchain, libc, and every transitive dependency. A cache hit is therefore an exact-environment hit, not "same package version, maybe different system libraries."
CLI / UX ergonomics
The flake CLI is the nix "new CLI" (also experimental: nix-command). The command boundary is a flake reference plus a #-fragment attribute path, with sensible defaults filled from the standard output schema:
nix build .#core-cli # build packages.<system>.core-cli of the local flake
nix build # … .default (getDefaultFlakeAttrPaths)
nix run .#backend -- --serve # run apps.<system>.backend
nix develop .#core-cli # enter that package's dev shell
nix flake show # tree of all outputs (the schema, rendered)
nix flake check # evaluate + build every checks.<system>.<name>
nix flake metadata # resolved/locked URL + the input tree
nix build github:NixOS/nixpkgs#hello # a remote flake, no checkoutLock-file ergonomics are a small, sharp command family (flake-update.md):
| Command | Effect |
|---|---|
nix flake lock | create/extend the lock; never changes already-locked inputs |
nix flake update | re-lock all inputs from scratch |
nix flake update nixpkgs | update only the named input(s) — targeted slicing |
nix flake update --flake ~/other | operate on a flake in another directory |
nix build … --override-input X ref | one-shot input substitution without touching the lock |
NOTE
nix flake update <input> "takes a list of names of inputs to update as its positional arguments" (flake-update.md) — this is the flake analogue of cargo update -p <crate> / pnpm --filter: targeted re-locking of one member's pin without disturbing the rest of the graph.
The ergonomic cost is the fragment vs. flake-ref split (.#attr everywhere), the requirement to enable two experimental features, and the fact that the unit of selection is an output attribute, not a "package by name with a filter flag." There is no --filter/-p/--since family; selection happens by which attribute path (and thus which sub-flake or sub-derivation) you name.
Strengths
- True polyglot reproducibility. One lock file pins a Rust crate snapshot, a C library, a nixpkgs revision, and sibling flakes — by content hash, across machines — which no single-language tool can do.
- Total isolation, no hoisting. Content-addressed store paths eliminate the shared-mutable-
node_modules/targetclass of bugs outright; conflicting dependency versions simply get distinct paths. - Best-in-class caching. Local store + organisation/public binary caches give exact-environment cache hits for arbitrary builds, plus separate evaluation caching via the locked-flake
Fingerprint. - Sound change detection. The rebuild decision is a hash of the real build closure, not a user-declared task input list, so it cannot silently miss a dependency.
- Uniform discoverable schema.
packages/devShells/checks/appsletnix build/run/develop/flake checkwork on any flake with no config. - Composable graph with
follows/overrides. Transitive dedup and override give precise control over a large, shared dependency graph. - Distributed/remote builds via
--buildersand Hydra, plus cross-arch remote builders.
Weaknesses
- The Nix language is the price of admission. Everything — manifest, build rules, glue — is written in a lazy purely-functional DSL with a famously steep learning curve, unfamiliar to most ecosystem developers.
- Still "experimental." Flakes have shipped in production for years but remain gated behind the
flakes+nix-commandexperimental features (experimental-features.cc); the surface can still change. - No native workspace/member globbing. Monorepo members are enumerated by hand under
inputs; there is nomembers = ["libs/*"], and the relative-path cross-ref discipline (path:./fooonly, never../) is restrictive. - No task-pipeline layer. No named tasks with
dependsOn, no--filter, no affected-since-HEAD~1selection at the flake layer; you express slicing by which attribute path you build. - Pinned, not ranged. No SemVer solver — updates are wholesale or per-input re-locks, which is reproducible but can mean coarse, manual bumps.
- Operational weight. A populated
/nix/store, the daemon, and binary-cache configuration are heavier than dropping apackage.jsoninto a repo. - Fragmented governance. Upstream Nix, Lix, and Determinate Nix are three implementations of the same flake schema — convergent today, but a long-term coordination risk.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Topology via a typed inputs graph (no [workspace]) | One uniform node type (the flake) composes recursively; remote + local treated alike | No member globbing; monorepo members listed by hand; relative-path cross-refs are limited |
Pin inputs to exact rev + narHash (no version solver) | Bit-for-bit reproducibility; lock is substitutable from a cache | No "compatible range" resolution; updates are wholesale or per-input, sometimes coarse |
| Lock file as a graph isomorphic to the dep graph | De-dupes shared transitive inputs; supports cycles; follows rewrites edges cheaply | More complex than a flat list; the graph/follows semantics confuse newcomers |
outputs is a pure lazy function over realised inputs | Demand-driven topological evaluation; side-effect-free; cacheable by Fingerprint | Requires expressing everything in the Nix language; debugging laziness is hard |
| Content-addressed store, no hoisting | Total isolation; conflicting versions coexist; exact-environment cache hits | A large /nix/store and daemon; disk-heavy vs. a hoisted symlink tree |
| Caching at the build closure, not the package version | Cache hits include the toolchain/libc — genuinely reproducible across machines | Larger cache keys; a toolchain bump invalidates everything downstream |
| Flakes gated behind an experimental feature | Lets the schema stabilise (lock version 7) before a stability commitment | Years of "experimental" friction; every invocation needs the feature flags enabled |
CLI selects by #attrpath, not --filter/-p | The uniform output schema is the addressing scheme; remote and local targets identical | No affected-detection / --since; coarser than a dedicated task orchestrator's filter set |
What dub could borrow
- A content-hash-pinned, graph-shaped lock file.
flake.lock'soriginal→locked(rev+narHash) per node, isomorphic to the dependency graph, is a far stronger reproducibility story thandub.selections.json's flat version map. Adubworkspace lock that records resolved commits/hashes and de-duplicates shared transitive deps would pin a whole monorepo exactly. follows-style transitive dedup. Letting adubworkspace force every member's transitivevibe-d(etc.) to one pinned node — the flakefollowsoperator — directly addresses the version-drift the proposal targets, without a central[workspace.dependencies]solver.- Relative-path local cross-refs as a first-class, lockable input. Nix's
path:./libs/core-cli(reusing the parent's fetched tree, pinned bynarHash) is exactly the local-first cross-referencedubwants instead of bare relativepath=overrides — depend on a neighbour by identity and lock it. - Evaluation caching keyed by a fingerprint of the locked inputs. Nix's
LockedFlake::getFingerprint(locked ref + subdir + lock hash) shows how to skip re-resolution/re-evaluation when the lock is unchanged — a cheap win for repeateddub build/dub testacross a large workspace.
Sources
- NixOS/nix — GitHub repository
src/nix/flake.md— flake references,flake.nixschema, lock-file format (verbatim quotes above)src/libflake/flake.cc—lockFlake/computeLocks, the recursive locker;getFingerprintsrc/libflake/call-flake.nix— lazy recursive output evaluation,self/inputswiringsrc/libflake/lockfile.cc—flake.lockversion 7read/writesrc/libflake/flakeref.cc—FlakeRefparsingsrc/libflake/include/nix/flake/flake.hh—Flake,FlakeInput,LockFlags,LockedFlakesrc/libcmd/installable-flake.cc+installables.cc—flakeref#attrpath, default attr paths/prefixessrc/nix/flake-check.md— output schema enforced bynix flake checksrc/nix/flake-update.md— targeted vs. wholesale re-lockingsrc/libutil/experimental-features.cc— theflakesexperimental feature gate- RFC 49 — Flakes (the design RFC)
- nix.dev — Flakes guide
- Sibling deep-dives:
cargo·go-work·pnpm·npm·turborepo·nx·bazel·buck2·buildbarn·buildbuddy·nativelink· thedubbaseline · the D landscape