Day 8 — First project flake

Updated

July 30, 2026

Day 8 — First project flake

Stage I · ~4h (theory + project)
Goal: Author a real project flake with pinned inputs, clear outputs, committed flake.lock, and a working devShells.default—before any NixOS host work.

Why this day exists

Flakes are the modern collaboration unit: reproducible inputs + named outputs. A devShell is the highest-value first output—shared tools without polluting a global profile. OS configuration comes in Stage II; project pinning comes now.


Theory 1 — What a flake is

A flake is a directory (usually a git tree) with:

File Role
flake.nix Declarative inputs + outputs function
flake.lock Exact pinned revisions/hashes of inputs

Evaluation model

flake.nix outputs = inputs: { … }
                 ↓
        lock pins input trees
                 ↓
     outputs provide packages, shells, apps, nixosConfigurations, …

Punchline: without the lock file, “same flake.nix” is not the same system.


Theory 2 — inputs

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
    # optional:
    # flake-utils.url = "github:numtide/flake-utils";
  };
}

Common URL forms

URL Meaning
github:Owner/Repo Default branch (avoid for pins—use lock)
github:Owner/Repo/ref Branch/tag/rev
git+https://… Arbitrary git
path:./vendor/x Local path input

Following transitive inputs

inputs.foo.inputs.nixpkgs.follows = "nixpkgs";

Keeps a single nixpkgs when dependencies would otherwise multiply.


Theory 3 — outputs

outputs = { self, nixpkgs, ... }@inputs:
  let
    system = "x86_64-linux";
    pkgs = import nixpkgs { inherit system; };
  in {
    packages.${system}.default = pkgs.hello;
    devShells.${system}.default = pkgs.mkShell {
      packages = [ pkgs.git pkgs.hello pkgs.jq ];
    };
  };

Output schema (common)

Output Purpose
packages.<system>.<name> Buildable packages
devShells.<system>.<name> nix develop environments
apps.<system>.<name> nix run apps
formatter.<system> nix fmt
checks.<system>.<name> nix flake check
nixosConfigurations.<name> NixOS hosts (Stage II)
overlays.default Package overlays (later)

self

self is the flake’s own source (after copy to store). Used for src = self; packaging and referring to other outputs: self.packages.${system}.foo.


Theory 4 — Systems and portability

Hard-coding one system is fine for a first lab. Better patterns later: flake-utils, flake-parts, or explicit map over systems.

systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];

For today: pick your system correctly or multi-define the same shell for two systems if you dual-boot.

Discover:

nix eval --impure --expr 'builtins.currentSystem'

Theory 5 — mkShell / mkShellNoCC

pkgs.mkShell {
  packages = [ pkgs.go pkgs.git ];
  shellHook = ''
    echo "welcome to the dev shell"
    export DEMO=1
  '';
}
Field Role
packages / nativeBuildInputs Tools on PATH
buildInputs Libraries (more package-oriented)
shellHook Runs on enter

Prefer putting project tools here, not every CLI you like globally.


Theory 6 — Lock discipline (intro)

nix flake lock          # ensure lock exists/up to date with urls
nix flake update        # bump inputs (all)
nix flake update nixpkgs  # bump one input
Rule Why
Commit flake.lock Reproducibility
Review lock diffs Security + surprise upgrades
Selective update Smaller change sets

Day 24 goes deeper; today: lock must exist and be committed.


Theory 7 — Flakes and git

Flakes generally want a git repository. Untracked files may be invisible to evaluation depending on setup.

git init
git add flake.nix
# after first lock:
git add flake.lock

If something “missing,” check git status—not only the filesystem.


Theory 8 — Registries (light awareness)

nix registry list

nixpkgs#hello resolves via registry → often a floating ref until you pin your project’s own input. Project flakes should not rely on registry drift; they declare inputs.nixpkgs.url.


Worked examples bank

Example A — Minimal full flake

{
  description = "day08 project shell";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
    in {
      devShells = forAllSystems (system:
        let pkgs = import nixpkgs { inherit system; };
        in {
          default = pkgs.mkShell {
            packages = with pkgs; [ git hello jq ];
          };
        });
    };
}

Example B — Enter and verify

nix develop
hello
git --version
jq --version
exit

Example C — Metadata

nix flake metadata
nix flake show

Lab 1 — Create the project

mkdir -p ~/lab/90daysofx/02-nixos/day08/myproject
cd ~/lab/90daysofx/02-nixos/day08/myproject
git init

Write flake.nix (start from Example A; trim systems if you want).

nix flake lock
nix flake show
nix develop -c hello
git add flake.nix flake.lock
git commit -m "day08: initial project flake with devShell"

Lab 2 — Make it your project

Add tools you actually want for a small project (pick a lane):

Lane Example packages
Go go, gopls, git
Rust rustc, cargo, rust-analyzer
Python python3, uv or poetry
Ops jq, ripgrep, kubectl (if desired)

Edit packages = [ … ], re-enter shell, verify versions.

Add a trivial project file README.md describing how to enter the shell.


Lab 3 — shellHook and env

shellHook = ''
  export PROJECT_ROOT="$PWD"
  echo "devShell ready for $PROJECT_ROOT"
'';
nix develop -c bash -c 'echo $PROJECT_ROOT'

Lab 4 — Offline-capable after populate (preview of gate)

  1. Enter nix develop once online (populate store).
  2. Note nix flake metadata resolved revisions.
  3. Optional: airplane mode / disconnect and nix develop --offline (or equivalent) if supported in your setup.

Journal what still requires network (usually nothing if all paths realized).


Lab 5 — Teach-back

  1. Why flake.lock is mandatory in git
  2. Difference between packages and devShells
  3. What nix develop uses by default
  4. Why registry nixpkgs# ≠ your flake’s pinned nixpkgs

Common gotchas

Symptom Theory
File not found in flake eval Not git-added
Wrong system attr devShells.<system> mismatch
nix develop empty No default shell
Divergent tools across machines Lock not committed / not pulled
Huge shell closure Too many packages; split shells later
Dirty tree warnings Commit or understand copy semantics

Checkpoint

  • flake.nix with nixpkgs 26.05 input
  • flake.lock committed
  • nix develop provides ≥2 real tools
  • nix flake show lists devShell
  • README explains entry
  • Understand lock vs registry

Commit

cd ~/lab/90daysofx/02-nixos/day08/myproject
git add .
git commit -m "day08: first project flake devShell lock"

Tomorrow

Day 9 — Classic CLI literacy. Map old nix-build / nix-shell / channels onto modern commands once so old docs stop scaring you.