Day 8 — First project flake
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.lockIf something “missing,” check git status—not only the filesystem.
Theory 8 — Registries (light awareness)
nix registry listnixpkgs#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
exitExample C — Metadata
nix flake metadata
nix flake showLab 1 — Create the project
mkdir -p ~/lab/90daysofx/02-nixos/day08/myproject
cd ~/lab/90daysofx/02-nixos/day08/myproject
git initWrite flake.nix (start from Example A; trim systems if you want).
nix flake lock
nix flake show
nix develop -c hellogit 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)
- Enter
nix developonce online (populate store).
- Note
nix flake metadataresolved revisions.
- 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
- Why
flake.lockis mandatory in git
- Difference between
packagesanddevShells
- What
nix developuses by default
- 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.nixwith nixpkgs 26.05 input
flake.lockcommitted
nix developprovides ≥2 real tools
nix flake showlists 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.