Project 3: Language package derivation

Updated

July 30, 2026

Project 3 — Package a Go or Python app with language helpers

Goal: Use a language-specific builder (buildGoModule or buildPythonApplication) so fetch/build/test idioms match nixpkgs conventions. Finish one language end-to-end with real hashes (not fakeHash left behind).

Field Value
Baseline Nix 2.34, nixpkgs nixos-26.05
Time ~2–5 hours
Depends on Projects 1–2; the language ecosystems A chapter–48 helpful
Artifact packages.<system>.default + REPORT.md

Why this project exists

stdenv.mkDerivation can package anything, but language ecosystems already have conventions: Go module vendor hashes, Python pyproject hooks, test integration. nixpkgs helpers encode those conventions so your package looks like every other good citizen in the tree.

This project teaches:

  • How fixed-output-style hashes pin dependency graphs (vendorHash, cargo-like patterns, FOD src).
  • How to read the first failed build that prints the expected hash.
  • How language tests map onto doCheck / hooks.

Pick Option A (Go) or Option B (Python) and complete it. Doing both is excellent stretch, not required for green.


Prerequisites

  • Project 1 flake layout comfort.
  • Toolchain on the host optional for scaffolding (go / python); pure Nix builds should still work after hashes are set.
  • Network once for module download / first hash discovery (unless you vendor offline).

Theory map

Concern Go (buildGoModule) Python (buildPythonApplication)
Manifest go.mod / go.sum pyproject.toml / setup.cfg
Dep pin vendorHash or vendor/ Package set + optional FODs
Entry compiled binary → $out/bin console_scripts / scripts
Tests doCheck, checkFlags pytestCheckHook, unittest
Common footgun wrong vendorHash / proxy missing format/pyproject, native deps

Why helpers beat raw stdenv here

Language builders set:

  • Correct nativeBuildInputs (compilers, python, hooks).
  • Dependency fetch phases that honor hashes.
  • Install layouts that match ecosystem expectations.

You still need to understand stdenv phases—the helper fills them.

Hash discipline

Field Meaning
lib.fakeHash / pkgs.lib.fakeSha256 Placeholder that must be replaced
Hash in build error Authoritative for that input content
Changing src or deps Must recompute related hashes

Never commit a known-fake hash as “done.”


Lab tree (shared idea)

~/lab/nixos-projects/lang-pkg/
  flake.nix
  flake.lock
  pkg.nix
  REPORT.md
  # Go:
  go.mod go.sum main.go
  # or Python:
  pyproject.toml hi_py/__init__.py hi_py/__main__.py
mkdir -p ~/lab/nixos-projects/lang-pkg
cd ~/lab/nixos-projects/lang-pkg

Option A — Go (buildGoModule)

A1. Scaffold module

go mod init example.com/hi

main.go:

package main

import "fmt"

func main() {
    fmt.Println("hi from buildGoModule")
}

Optional dependency so vendorHash is non-trivial (recommended):

package main

import (
    "fmt"
    "github.com/google/uuid"
)

func main() {
    fmt.Println("hi from buildGoModule")
    fmt.Println(uuid.NewString())
}
go get github.com/google/uuid@v1.6.0
go mod tidy

A2. Package expression

pkg.nix:

{ buildGoModule, lib }:

buildGoModule {
  pname = "hi";
  version = "0.1.0";
  src = lib.cleanSource ./.;

  # First build: set to lib.fakeHash and read the error.
  # After vendor/ present, some pins use vendorHash = null — follow your nixpkgs docs.
  vendorHash = lib.fakeHash;

  meta = {
    description = "Project 3 Go lab binary";
    license = lib.licenses.mit;
    mainProgram = "hi";
  };
}

A3. Flake

{
  description = "lang-pkg Go — Project 3";

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

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
      forAll = nixpkgs.lib.genAttrs systems;
    in {
      packages = forAll (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in {
          default = pkgs.callPackage ./pkg.nix { };
        });

      apps = forAll (system: {
        default = {
          type = "app";
          program = "${self.packages.${system}.default}/bin/hi";
        };
      });
    };
}

A4. Hash fix loop

nix build 2>&1 | tee /tmp/go-build.log
# Look for: specified: sha256-AAAA... got: sha256-REAL...
# Paste REAL into vendorHash

# rebuild until green
nix build -L
nix run
./result/bin/hi

A5. Vendor / tests (optional)

go mod vendor   # then set vendorHash per nixpkgs 26.05 docs (often null when vendored)

Light test: main_test.go + doCheck = true so a failing test fails the derivation.


Option B — Python (buildPythonApplication)

Layout:

pyproject.toml
hi_py/__init__.py
hi_py/__main__.py

pyproject.toml (setuptools; hatchling also fine if hooks match your pin):

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "hi-py"
version = "0.1.0"
description = "Project 3 Python lab"
requires-python = ">=3.11"
dependencies = []

[project.scripts]
hi-py = "hi_py.__main__:main"

[tool.setuptools.packages.find]
include = ["hi_py*"]

hi_py/__main__.py:

def main() -> None:
    print("hi from buildPythonApplication")


if __name__ == "__main__":
    main()

pkg.nix:

{ python3Packages, lib }:

python3Packages.buildPythonApplication {
  pname = "hi-py";
  version = "0.1.0";
  src = lib.cleanSource ./.;

  pyproject = true;
  build-system = [ python3Packages.setuptools ];

  meta = {
    description = "Project 3 Python lab";
    license = lib.licenses.mit;
    mainProgram = "hi-py";
  };
}
Warning

Python attrs evolve (pyproject = true vs older format). Match a simple package on your 26.05 pin if eval drifts. Goal: helper lifecycle, not one frozen attr name.

nix build -L && nix run && ./result/bin/hi-py

Recommended: add one dep (click in pyproject.toml and dependencies = [ python3Packages.click ];), rebuild, confirm with nix path-info -r ./result | rg -i click.


Shared finish

nix flake show
nix path-info -rS ./result | tail
nix path-info -Sh ./result

REPORT.md (required)

# Project 3 report
## Language + helper (why)
## Hashes / pins (what each field fingerprints)
## Closure (`nix path-info -Sh`)
## One failure + fix
## Offline rebuild story after FODs populate

Pitfalls

Symptom Likely cause Fix
Hash mismatch forever Edited src / go.sum after hash set Re-run hash loop
Go wants network in sandbox Bad vendor / vendorHash Fix per pin docs
Python missing module at runtime Dep not declared in Nix Add to dependencies
nix run misses binary Name ≠ mainProgram / scripts Align entry names
Dirty src .venv, result, __pycache__ cleanSource + gitignore

Acceptance criteria

  • nix run works for the chosen language binary.
  • Hash fields are real (no committed fakeHash).
  • REPORT.md complete (helper, hashes, closure, one failure).
  • Flake exposes packages.<system>.default.
  • Offline rebuild after FODs are populated (documented).
  • flake.lock committed.

Stretch goals

  1. Ship both Go and Python in one flake.
  2. Real unit test under doCheck.
  3. Rust track: rustPlatform.buildRustPackage + cargoHash.
  4. Diff your pkg.nix against a similar nixpkgs package (three style notes).

Self-check

  1. What does vendorHash fingerprint?
  2. Why both Python build-system and runtime dependencies?
  3. Where does the runnable live under $out for each option?
  4. How would you pin a bad upstream version until a fix? (→ Project 4)

Next: Project 4 — Overlay pin and patch.