Project 3: Language package derivation
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.”
Option A — Go (buildGoModule)
A1. Scaffold module
go mod init example.com/himain.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 tidyA2. 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/hiA5. 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";
};
}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-pyRecommended: add one dep (click in pyproject.toml and dependencies = [ python3Packages.click ];), rebuild, confirm with nix path-info -r ./result | rg -i click.
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 runworks for the chosen language binary.
- Hash fields are real (no committed
fakeHash).
REPORT.mdcomplete (helper, hashes, closure, one failure).
- Flake exposes
packages.<system>.default.
- Offline rebuild after FODs are populated (documented).
flake.lockcommitted.
Stretch goals
- Ship both Go and Python in one flake.
- Real unit test under
doCheck.
- Rust track:
rustPlatform.buildRustPackage+cargoHash.
- Diff your
pkg.nixagainst a similar nixpkgs package (three style notes).
Self-check
- What does
vendorHashfingerprint?
- Why both Python
build-systemand runtimedependencies?
- Where does the runnable live under
$outfor each option?
- How would you pin a bad upstream version until a fix? (→ Project 4)