diff --git a/02-package/flake.lock b/02-package/flake.lock new file mode 100644 index 0000000..682004d --- /dev/null +++ b/02-package/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/02-package/flake.nix b/02-package/flake.nix new file mode 100644 index 0000000..32b1155 --- /dev/null +++ b/02-package/flake.nix @@ -0,0 +1,47 @@ +{ + # Packages a shell script as a Nix derivation. + # Try: `nix build`, then `./result/bin/greet` + # Or: `nix run` + description = "Package a script with stdenv.mkDerivation"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + packages.${system}.default = pkgs.stdenv.mkDerivation { + pname = "greet"; + version = "0.1.0"; + + # `src = ./.` copies the flake directory into the store. + # The build then runs inside that copy. + src = ./.; + + # Skip the default unpack/build phases; this example has no C code. + dontUnpack = true; + dontBuild = true; + + # installPhase is a shell snippet. `$out` is the output store path. + installPhase = '' + mkdir -p $out/bin + cp ${./greet.sh} $out/bin/greet + chmod +x $out/bin/greet + + # Patch the shebang so the script uses a store-path bash, + # not /bin/sh (which may not exist on a pure NixOS system). + substituteInPlace $out/bin/greet \ + --replace-fail "#!/usr/bin/env bash" "#!${pkgs.bash}/bin/bash" + ''; + }; + + # `nix run` looks here. `program` must be a store path to an executable. + apps.${system}.default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/greet"; + }; + }; +} diff --git a/02-package/greet.sh b/02-package/greet.sh new file mode 100644 index 0000000..905a2c4 --- /dev/null +++ b/02-package/greet.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# A trivial script to be packaged by the flake. +name="${1:-world}" +echo "hello, ${name}! (built by nix)" diff --git a/03-multi-system/flake.lock b/03-multi-system/flake.lock new file mode 100644 index 0000000..682004d --- /dev/null +++ b/03-multi-system/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/03-multi-system/flake.nix b/03-multi-system/flake.nix new file mode 100644 index 0000000..355e67f --- /dev/null +++ b/03-multi-system/flake.nix @@ -0,0 +1,52 @@ +{ + # Makes outputs available on multiple platforms using a hand-rolled + # `forAllSystems` helper (no extra dependencies). + # Compare with the `flake-utils` approach described in notes/005-multi-system.md. + description = "Multi-system outputs with forAllSystems"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }: + let + # List every system you want to support. + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + + # Helper: apply a function to each system and collect results into an attrset. + # `nixpkgs.lib.genAttrs` turns a list of keys + a function into { key = f key; … }. + forAllSystems = f: + nixpkgs.lib.genAttrs supportedSystems (system: + f (import nixpkgs { inherit system; }) + ); + in { + # Every output that varies per system goes through `forAllSystems`. + packages = forAllSystems (pkgs: { + default = pkgs.writeShellScriptBin "hello-multi" '' + echo "hello from $(uname -s)/$(uname -m)" + ''; + }); + + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = with pkgs; [ jq ripgrep ]; + shellHook = '' + echo "dev shell on $(uname -s)/$(uname -m)" + ''; + }; + }); + + # `nix flake check` evaluates everything under `checks`. + # Wrapping a simple test here shows the pattern. + checks = forAllSystems (pkgs: { + runs = pkgs.runCommand "hello-multi-runs" {} '' + ${self.packages.${pkgs.system}.default}/bin/hello-multi > $out + ''; + }); + }; +} diff --git a/notes/004-packaging.md b/notes/004-packaging.md new file mode 100644 index 0000000..ad2162b --- /dev/null +++ b/notes/004-packaging.md @@ -0,0 +1,90 @@ +# Packaging + +This note covers `02-package/`, which builds a shell script into a Nix store path using `stdenv.mkDerivation`. + +--- + +## 1. What `nix build` Does + +When you run `nix build` inside `02-package/`: + +1. Nix evaluates `flake.nix` and produces a `.drv` file (the build recipe). +2. Nix realises the `.drv`: it runs the install phase inside a sandbox with no network and no access to files outside the declared inputs. +3. The output lands in `/nix/store/-greet-0.1.0/`. +4. A `./result` symlink is created pointing to that store path. + +You can then run `./result/bin/greet` directly, or use `nix run` which does step 1 through 3 and then executes the `apps..default.program` path. + +--- + +## 2. `stdenv.mkDerivation` in Detail + +`stdenv.mkDerivation` is the main builder in nixpkgs. It wraps the low-level `derivation` builtin with a phased build system. The default phases, in order: + +1. `unpackPhase`: extracts `src` (tarball, git checkout, or copied path). +2. `patchPhase`: applies patches from the `patches` list. +3. `configurePhase`: runs `./configure` or cmake or similar. +4. `buildPhase`: runs `make` or equivalent. +5. `installPhase`: copies outputs into `$out`. +6. `fixupPhase`: strips binaries, patches RPATHs, shrink-wraps references. + +For a simple script, most of these are irrelevant. The `02-package` example disables unpack and build with `dontUnpack = true` and `dontBuild = true`, then writes a custom `installPhase`. + +--- + +## 3. Key Concepts in the Example + +`$out`: every derivation has at least one output. `$out` is the default output path. The build must place files under `$out` or the derivation produces nothing. + +`substituteInPlace`: a nixpkgs helper that does in-place string replacement. The example uses it to rewrite the shebang from `#!/usr/bin/env bash` to a store-path bash. This matters because `/usr/bin/env` may not exist on a pure NixOS system, and even where it does, it would pick up whichever `bash` happens to be on `$PATH` at runtime rather than the pinned one. + +`src = ./.`: copies the entire flake directory into the store before the build starts. Changes to any file in the directory change the hash, which triggers a rebuild. For real projects you'd use `lib.fileset` or `lib.cleanSource` to exclude irrelevant files (editor backups, `.git/`, etc.). + +`apps..default`: the `nix run` command looks for this attribute. Its `program` field must be an absolute store path to an executable. + +--- + +## 4. The Build Sandbox + +By default, Nix builds run inside a sandbox: + +- No network access (except fixed-output derivations). +- No access to the host filesystem beyond explicitly declared inputs. +- No access to environment variables from the host. +- A restricted `/tmp` for scratch space. + +This is what guarantees reproducibility: the build can only use what it declared. + +If a build needs to download something (a source tarball, a Go module cache), it must be a fixed-output derivation. You provide the expected hash, and Nix verifies the output matches. See `builtins.fetchurl`, `pkgs.fetchFromGitHub`, and similar. + +--- + +## 5. Commands to Try + +```bash +cd 02-package + +nix build # build and create ./result symlink +./result/bin/greet # run directly +./result/bin/greet "nix learner" # pass an argument + +nix run # build + run via apps.default +nix run . -- "nix learner" # pass arguments after -- + +nix path-info ./result # show the store path +nix path-info -rS ./result # show closure size (all transitive deps) +nix derivation show ./result # inspect the .drv (inputs, env, builder) +``` + +--- + +## 6. Beyond Shell Scripts + +The same `stdenv.mkDerivation` pattern scales to compiled languages. nixpkgs provides wrappers: + +- `rustPlatform.buildRustPackage` for Rust (Cargo). +- `buildGoModule` for Go. +- `python3Packages.buildPythonPackage` for Python. +- `mkDerivation` directly for C/C++ (autotools, cmake, meson). + +Each wrapper pre-configures the phases for its ecosystem. You supply `src`, a lock file hash, and metadata; the wrapper handles configure/build/install. diff --git a/notes/005-multi-system.md b/notes/005-multi-system.md new file mode 100644 index 0000000..829411b --- /dev/null +++ b/notes/005-multi-system.md @@ -0,0 +1,118 @@ +# Multi-System Outputs + +This note covers `03-multi-system/`, which makes a flake's packages, dev shells, and checks available on multiple platforms from a single `flake.nix`. + +--- + +## 1. The Problem + +Flake outputs are keyed by system: `packages.x86_64-linux.default`, `packages.aarch64-darwin.default`, and so on. If you hard-code one system (as `01-devshell` and `02-package` do), the flake only works on that platform. + +To support multiple systems, you need to generate the per-system attrset for each platform you care about. + +--- + +## 2. The `forAllSystems` Pattern + +The `03-multi-system` example defines a local helper: + +```nix +forAllSystems = f: + nixpkgs.lib.genAttrs supportedSystems (system: + f (import nixpkgs { inherit system; }) + ); +``` + +`genAttrs` takes a list of keys and a function, and returns `{ key1 = f key1; key2 = f key2; … }`. Wrapping it so the callback receives `pkgs` (already imported for the right system) keeps the per-system output definitions clean: + +```nix +packages = forAllSystems (pkgs: { + default = pkgs.writeShellScriptBin "hello-multi" '' + echo "hello from $(uname -s)/$(uname -m)" + ''; +}); +``` + +This produces: + +``` +packages.x86_64-linux.default +packages.aarch64-linux.default +packages.aarch64-darwin.default +packages.x86_64-darwin.default +``` + +No external dependencies. You control the system list explicitly. + +--- + +## 3. The `flake-utils` Alternative + +`flake-utils` is a community flake that provides `eachDefaultSystem`, which does the same thing with less boilerplate: + +```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; + }); +``` + +Differences from the hand-rolled approach: + +- `eachDefaultSystem` covers a predefined list of common systems (currently `x86_64-linux`, `aarch64-linux`, `x86_64-darwin`, `aarch64-darwin`). You can use `eachSystem` to specify your own list. +- The callback returns a flat attrset (`{ packages.default = …; devShells.default = …; }`), and `eachDefaultSystem` nests it under each system automatically. +- It adds one input to `flake.lock`. Use `follows` to keep its nixpkgs in sync with yours. + +Both approaches are common. The hand-rolled helper is better when you want full control; `flake-utils` is better when you want less repetition. `flake-parts` is a third option for larger projects that benefit from a module system on top of flakes. + +--- + +## 4. The `checks` Output + +`03-multi-system` also demonstrates a `checks` output: + +```nix +checks = forAllSystems (pkgs: { + runs = pkgs.runCommand "hello-multi-runs" {} '' + ${self.packages.${pkgs.system}.default}/bin/hello-multi > $out + ''; +}); +``` + +`nix flake check` evaluates and builds everything under `checks.`. If any check derivation fails to build, the command exits non-zero. + +`runCommand` is a convenience wrapper around `mkDerivation` that runs a single shell snippet. It needs to produce `$out` (a file or directory) to succeed. Here it runs the built script and writes its output to `$out`, which proves the binary at least executes without error. + +This is the simplest form of a flake-level test. More involved checks might run a test suite, validate config syntax, or compare outputs against expected snapshots. + +--- + +## 5. Commands to Try + +```bash +cd 03-multi-system + +nix flake show # see outputs for all four systems +nix build # build default package for your system +nix run # build + run +nix flake check # build all checks (proves the script runs) + +# Inspect what other systems would get (eval only, no cross-build): +nix eval .#packages.aarch64-darwin.default.name +``` + +--- + +## 6. When Cross-System Matters + +Multi-system outputs are useful when: + +- You collaborate across macOS and Linux machines. +- CI runs on a different platform than your laptop. +- You publish a flake for others to consume (they expect their system to be present). +- You want `nix flake check` to evaluate outputs for every platform, catching typos that would only surface on the other OS. + +If you only ever build on one machine, a single hard-coded system string is fine. You can always add `forAllSystems` later without changing any output behavior on your own platform.