# Flakes A flake is a directory containing `flake.nix` (and, once evaluated, `flake.lock`). It's a packaging convention on top of regular Nix that gives you: - Pinned inputs via `flake.lock` for reproducible builds. - A fixed output schema so tools know where to look (`devShells..default`, `packages..default`, and so on). - Composability, since flakes depend on other flakes by URL. - Pure evaluation, with no silent dependence on `NIX_PATH`, env vars, or random files. Flakes are still marked "experimental" but are now the way most new Nix code is written. --- ## 1. Anatomy of a `flake.nix` ```nix { description = "…"; # optional human-readable summary inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Pattern: make a transitive input follow yours, to avoid duplicates flake-utils.url = "github:numtide/flake-utils"; flake-utils.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, flake-utils, ... }: { # arbitrary attrs here, but names with meaning to `nix` CLI must match the schema }; } ``` Two top-level keys matter: `inputs` and `outputs`. `description` is cosmetic. ### Inputs Each input has a flake reference URL. Common forms: ``` github:owner/repo # latest default branch github:owner/repo/branch-or-tag # specific ref github:owner/repo/abc123 # specific commit git+https://example.com/repo.git git+ssh://git@example.com/repo path:./local-flake # local path tarball+https://…/src.tar.gz nixpkgs # registry alias (resolves via `nix registry`) ``` This repository now has focused examples for several of those cases: - `39-flake-apps/` covers `apps.` outputs, - `40-path-inputs/` covers a local flake `path:` input, and - `41-non-flake-inputs/` covers a local `path:` input with `flake = false`. Non-flake sources (a repo without `flake.nix`): ```nix inputs.some-src = { url = "github:owner/repo"; flake = false; }; # Access raw files via self.inputs.some-src ``` `41-non-flake-inputs/` shows that pattern with a local source tree and a JSON file. ### Outputs A function: `{ self, ...inputs }: `. The attrset keys are *conventions* the `nix` CLI knows about: | Output path | What it is | Command | |------------------------------|--------------------------------------|-----------------------------------------| | `devShells..` | A `mkShell`, a dev environment | `nix develop [.#]` | | `packages..` | A derivation, a build target | `nix build [.#]` | | `apps..` | `{ type = "app"; program = …; }` | `nix run [.#]` | | `nixosConfigurations.` | `nixpkgs.lib.nixosSystem { … }` | `nixos-rebuild switch --flake .#` | | `homeConfigurations.` | home-manager config | `home-manager switch --flake .#` | | `nixosModules.` | reusable NixOS module | imported by other flakes | | `overlays.` | nixpkgs overlay | imported by other flakes | | `formatter.` | a formatter derivation | `nix fmt` | | `checks..` | derivations run by `nix flake check` | `nix flake check` | | `templates.` | scaffold for `nix flake init` | `nix flake init -t .#` | Default attrs `default` are picked when you omit the name: `nix build` ≡ `nix build .#default`. `.` is the platform triple: `x86_64-linux`, `aarch64-linux`, `aarch64-darwin`, or `x86_64-darwin`. Arbitrary attrs are still allowed alongside those schema keys. `lib` is a common convention for pure helper functions and data; `42-lib-outputs/` shows one focused example. --- ## 2. The Lockfile `flake.lock` pins the exact commit and NAR hash of every input, transitively. Generated or updated automatically on first use or via `nix flake update`. - Commit it. Without it, "my flake" means different things on different machines. - Update explicitly. `nix flake update` (all inputs) or `nix flake update nixpkgs` (one input). - Inspect. `nix flake metadata` prints the resolved URLs and revisions. - `follows` deduplicates: if both `flake-utils` and `foo` want their own `nixpkgs`, `follows = "nixpkgs";` forces them to use yours. Without this, you can end up with multiple nixpkgs copies and subtle version skew. --- ## 3. Multi-System Support A flake output has to enumerate systems explicitly. Two common patterns: ### (a) Manual `forAllSystems` ```nix outputs = { self, nixpkgs }: let systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; })); in { packages = forAllSystems (pkgs: { default = pkgs.hello; }); devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = [ pkgs.jq ]; }; }); }; ``` Zero external deps, explicit. ### (b) `flake-utils` / `flake-parts` ```nix inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { packages.default = pkgs.hello; devShells.default = pkgs.mkShell { packages = [ pkgs.jq ]; }; }); ``` Less boilerplate, at the cost of one dependency. For larger projects, `flake-parts` is increasingly popular: it gives you a module system for flakes themselves. --- ## 4. Flake URIs: `.#` Syntax Everywhere you point `nix` at a flake, the form is `#`: ```bash nix build . # current dir, packages..default nix build .#foo # packages..foo nix build github:owner/repo#foo # remote flake nix develop .#ci # devShells..ci nix run nixpkgs#hello # pkgs.hello from the nixpkgs registry flake nixos-rebuild switch --flake .#my-host ``` The `` portion is auto-inferred from your machine; you don't spell it out in the CLI. --- ## 5. Pure Evaluation: What You Lose, What You Gain Flakes evaluate in pure mode: - No reading `NIX_PATH` or ``. - No arbitrary `builtins.getEnv` or reading files outside the flake. - `builtins.currentTime` and `builtins.currentSystem` are blocked. - Source is pulled from the flake's input tree, not the working directory, unless you're in `self`. Files not tracked by git are invisible to the flake by default. This last point surprises people: `git add` a file before `nix build` can see it. (`nix build --impure` escapes this, but then you're not reproducible.) In exchange: anyone with your `flake.nix` plus `flake.lock` gets bit-identical evaluation. --- ## 6. Common Commands ```bash nix flake init # scaffold a flake.nix in current dir nix flake init -t github:nix-community/templates#rust nix flake show # tree of all outputs nix flake metadata # locked inputs + paths nix flake check # evaluate & build all `checks` outputs nix flake update # bump all inputs nix flake update nixpkgs # bump one input nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11 nix develop # enter devShells..default nix develop .#ci # named shell nix build # build packages..default → ./result nix run # run apps..default (or packages..default) ``` --- ## 7. Recurring Patterns Worth Recognizing ### Rust / Go / Python Dev Shell ```nix devShells.default = pkgs.mkShell { packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ]; env.RUST_LOG = "debug"; shellHook = ''export FOO=bar''; }; ``` ### Package a Local Source Tree ```nix packages.default = pkgs.rustPlatform.buildRustPackage { pname = "my-tool"; version = "0.1.0"; src = ./.; cargoLock.lockFile = ./Cargo.lock; }; ``` ### Expose a NixOS Module ```nix nixosModules.default = import ./module.nix; # Consumer does: imports = [ inputs.my-flake.nixosModules.default ]; ``` ### NixOS System Config as a Flake ```nix nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./configuration.nix ]; }; # Apply with: nixos-rebuild switch --flake .#my-laptop ``` ### Overlay as an Output ```nix overlays.default = final: prev: { my-patched-foo = prev.foo.overrideAttrs (old: { … }); }; ``` --- ## 8. Debugging Tips - `nix flake show --all-systems` to see outputs for every platform, not just yours. - `nix eval .#packages.x86_64-linux.default.drvPath` to get the `.drv` without building. - `nix eval --json .#devShells.x86_64-linux.default.buildInputs | jq` to inspect shell deps. - `--show-trace` on any command for full eval backtraces. - `nix repl .` loads your flake's outputs into a REPL: useful for poking at attrs. - If an input seems stuck on an old rev, check `flake.lock`; `nix flake update ` fixes it. --- ## 9. When Flakes Are Overkill - Running a one-off tool: `nix run nixpkgs#ripgrep`, no flake needed. - A tiny personal script: plain `default.nix` plus `nix-build` is still fine. - Large monorepos with complex CI: consider `flake-parts` or hybrid setups (flake for the interface, regular Nix for guts). Flakes are a packaging interface, not a requirement. The old non-flake world still works; flakes just compose better once you have more than one Nix project.