Hedronite Lesson · Polyglot-Dev / Nix · Mon 2026-10-05

Packaging a thread-pool web server derivation, devShell, flake check

Beat A built the book's server. Beat B gives it a store path, notices it reads two files at runtime, and installs them too.

Lesson Class: Duha (Nix packaging shell · Beat B · topic T1)
Focus: buildRustPackage + postInstall · devShell with curl · checks entry
Code Blocks: derivation sketch, mkShell, checks attribute set
Done-criteria: Can sketch a buildRustPackage for the web server that installs its HTML, open a Rust devShell, and name one flake checks entry that proves the build
Grounding: rust-tops cargo-tops.nix + flake.nix · TRPL ch21-01 · Asr #16/#19 unmarked · no flake check run
The Crate Derivation
buildRustPackage for hello, with postInstall copying hello.html and 404.html to $out/share/hello.
The Dev Shell
mkShell with the toolchain and curl; cargo run from the crate root.
The Flake Check
checks.hello builds, tests, and installs; it does not start the server.
Beat A built the book's server. Beat B gives it a store path, notices it reads two files at runtime, and installs them too.

<!-- hal:authoritative:yaml -->

Beat A built the book's server. Beat B gives it a store path, notices it reads two files at runtime, and installs them too.

§I - Frame

Duha Beat B, Nix packaging session 07. Beat A (Rust session 24) walked TRPL Ch.21: a request-line server, a four-thread pool, and a Drop that closes the channel and joins. This shell packages that crate, named hello as in the book.

Session 06 packaged a print-and-exit binary. This one is different in one way that matters to Nix: handle_connection calls fs::read_to_string("hello.html") at request time. The HTML is runtime data, not source the compiler consumes. A derivation that ships only $out/bin/hello builds green and serves nothing.

Asr owns the Pills cursor and the Rust-roads rows (#16 crate-as-derivation, #19 flake devShell/checks). Those Done cells stay unmarked.

Three moves land by the end:

  1. The Crate Derivation: buildRustPackage for hello, with a postInstall that carries the two HTML files.
  2. The Dev Shell: devShells.default as the place you run cargo run and curl.
  3. The Flake Check: a checks entry that builds the package and runs its tests, and what it does not prove.

Done-criteria: Can sketch a buildRustPackage for the web server that installs its HTML, open a Rust devShell, and name one flake checks entry that proves the build.

§II - The Crate Derivation

The rust-tops corpus nix/cargo-tops.nix is still the skeleton: pname, version, src = lib.cleanSource ../., cargoLock.lockFile, doCheck = true, and a meta.mainProgram. Read-only teaching from the repo; no build cadence attaches to it.

For the web server, keep that skeleton and add one phase:

{ lib, rustPlatform }:

rustPlatform.buildRustPackage {
  pname = "hello";
  version = "0.1.0";

  src = lib.cleanSource ./.;

  cargoLock.lockFile = ./Cargo.lock;

  doCheck = true;

  postInstall = ''
    mkdir -p $out/share/hello
    cp hello.html 404.html $out/share/hello/
  '';

  meta.mainProgram = "hello";
}

cargoLock.lockFile pins the crate graph. The book's server is std only, so the lock lists one package and the builder fetches nothing. doCheck runs cargo test in the sandbox. Beat A's two unit tests drain eight jobs through Drop and assert new(0) panics. Neither opens a socket, which keeps the check honest inside a build sandbox.

postInstall runs after the binary lands in $out/bin. It copies the pages to $out/share/hello. The binary still opens hello.html relative to its working directory, so you run it from that folder: cd $(nix build --print-out-paths)/share/hello && ../../bin/hello. A later version would take the directory as an argument. Changing the Rust is Beat A's call, not this shell's.

Do not copy buildAndTestSubdir from the corpus. This crate is a single package, not a workspace member.

§III - The Dev Shell

The release derivation is not where you edit. devShells.default puts the toolchain on PATH without making it a runtime input of hello:

devShells = forAllSystems (
  system:
  let
    pkgs = pkgsFor system;
  in
  {
    default = pkgs.mkShell {
      packages = [
        pkgs.rustc
        pkgs.cargo
        pkgs.clippy
        pkgs.rustfmt
        pkgs.curl
      ];
    };
  }
);

That is the rust-tops flake.nix pattern with one addition, curl, because this crate is checked by hand with requests. Enter with nix develop, then cargo run from the crate root, where hello.html lives. In a second terminal, curl -i http://127.0.0.1:7878/ should show HTTP/1.1 200 OK and a Content-Length. /nope should show 404 NOT FOUND. After two connections the Listing 21-25 main returns and the pool prints its shutdown lines.

The shell gives you a pinned compiler and a client. The thread pool, the channel, and the Drop order live in Rust. The shell is the edit path, not $out.

§IV - The Flake Check

packages ships, checks proves. The corpus flake points its check at its own package:

checks = forAllSystems (
  system:
  {
    hello = self.packages.${system}.hello;
  }
);

nix flake check builds that attribute, which compiles hello, runs cargo test because doCheck is on, and runs postInstall. A missing 404.html would fail the cp and fail the check. That is the one runtime-file mistake this check catches.

It does not start the server. A tighter check would be a runCommand that launches hello from $out/share/hello, sends two requests, and asserts 200 then 404. Loopback inside a build sandbox varies by platform, so treat that as a separate, deliberate check, not a default.

This lesson does not claim a flake check ran. nix is on the lab Mac. No throwaway nix flake check was executed for this fire, so there is no exit code to report.

§V - Proof and close

  1. Copy the derivation and say what postInstall adds that buildRustPackage would not install on its own.
  2. Say why the two unit tests are safe under doCheck and a socket test might not be.
  3. Open the devShell, name the two curl calls, and the status line each should return.
  4. Add checks.hello and name the one runtime-file mistake it would catch.
  5. Name the Asr NIX-SYLLABUS rows that teach these moves later (#16, #19) and confirm this bundle did not mark them.

Done-criteria: Can sketch a buildRustPackage for the web server that installs its HTML, open a Rust devShell, and name one flake checks entry that proves the build.

This is the last Beat B on a TRPL row. Beat B ends with the dual-fire shape on 2026-10-10.

Related