Day 47 — Language ecosystem A
Day 47 — Language ecosystem A
Stage V · ~4h (lab-heavy)
Goal: Package a real tool in your primary language using the nixpkgs language builder (buildGoModule, buildRustPackage, buildPythonApplication, …)—not raw stdenv unless the ecosystem forces it.
Pick one primary ecosystem today. Day 48 is a second, smaller transfer exercise. Depth beats collecting half-broken scaffolds for five languages.
Why this day exists
stdenv taught phases. Production packaging is almost always ecosystem helpers: they know how to vendor deps, set LDFLAGS, wrap interpreters, and place modules. Mastering one helper end-to-end is how Stage V becomes useful at work.
Theory 1 — How language builders relate to stdenv
buildGoModule / buildRustPackage / buildPythonApplication / …
│
▼
stdenv.mkDerivation (+ setup hooks)
│
▼
phases + FODs (module vendor hashes, cargoLock, …)
You still have pname, version, meta, patches, and hooks—but fetching language deps is specialized.
| Ecosystem | Primary builder (nixpkgs) | Extra hash / lock concept |
|---|---|---|
| Go | buildGoModule |
vendorHash |
| Rust | rustPlatform.buildRustPackage |
cargoHash / cargoLock |
| Python | buildPythonApplication / buildPythonPackage |
pyproject / fetchPypi / overrides |
| Node | buildNpmPackage / importNpmLock |
npmDepsHash |
| Zig | buildZigPackage (where available) / stdenv |
Fetch deps carefully |
| C/C++ | stdenv + cmake/meson |
Usually no vendor hash |
Theory 2 — Go path (buildGoModule) — default if you know Go
{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "hello-go";
version = "0.1.0";
src = fetchFromGitHub {
owner = "you";
repo = "hello-go";
rev = "v${version}";
hash = "sha256-…";
};
vendorHash = "sha256-…"; # FOD of the vendored module graph
ldflags = [
"-s" "-w"
"-X main.version=${version}"
];
meta = {
description = "Tiny Go greeter";
license = lib.licenses.mit;
mainProgram = "hello-go";
};
}vendorHash workflow
- Set
vendorHash = lib.fakeHash;(ornullonly if truly vendorless—rare for modules).
- Build; read
got:hash.
- Paste; rebuild.
- When
go.mod/go.sumchange, vendorHash changes.
nix build .#hello-go -LUseful knobs
| Attr | Purpose |
|---|---|
subPackages |
Build only ./cmd/foo |
proxyVendor |
Vendor via module proxy inside FOD |
doCheck = false |
Skip tests if network-bound (prefer fixing tests) |
env.CGO_ENABLED = "0" |
Static-ish pure Go when appropriate |
Theory 3 — Rust path (buildRustPackage)
{ lib, rustPlatform, fetchFromGitHub }:
rustPlatform.buildRustPackage rec {
pname = "hello-rs";
version = "0.1.0";
src = fetchFromGitHub {
owner = "you";
repo = "hello-rs";
rev = "v${version}";
hash = "sha256-…";
};
cargoHash = "sha256-…";
meta = {
description = "Tiny Rust greeter";
license = lib.licenses.mit;
mainProgram = "hello-rs";
};
}Alternatively lock from a committed Cargo.lock with cargoLock = { lockFile = ./Cargo.lock; }; patterns used widely in nixpkgs—follow current nixpkgs examples for 26.05 if cargoHash vs git deps bit you.
Git dependencies in Cargo
Vendoring git crates often needs cargoLock.outputHashes for each git source. Expect Day 51-level debugging the first time.
Theory 4 — Python path (application vs library)
{ lib, python3Packages, fetchPypi }:
python3Packages.buildPythonApplication rec {
pname = "hello-py";
version = "0.1.0";
pyproject = true;
src = fetchPypi {
inherit pname version;
hash = "sha256-…";
};
build-system = with python3Packages; [ setuptools ];
dependencies = with python3Packages; [ click ];
meta = {
description = "Tiny Click CLI";
license = lib.licenses.mit;
mainProgram = "hello-py";
};
}| Builder | When |
|---|---|
buildPythonApplication |
CLI / end-user tool on PATH |
buildPythonPackage |
Library for others to depend on |
Prefer existing python3Packages.* deps over vendoring wheels by hand.
Theory 5 — Node (buildNpmPackage) sketch
{ lib, buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
pname = "hello-node";
version = "0.1.0";
src = fetchFromGitHub { /* … */ hash = "sha256-…"; };
npmDepsHash = "sha256-…";
meta = { mainProgram = "hello-node"; license = lib.licenses.mit; };
}package-lock.json (or supported lock) must exist; hash tracks the npm dependency FOD.
Worked example — flake with Go tool + check
{
description = "Day 47 language ecosystem A";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux"; # or genAttrs
pkgs = import nixpkgs { inherit system; };
in {
packages.${system}.default = pkgs.callPackage ./pkgs/hello-go.nix { };
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/hello-go";
};
checks.${system}.hello-go-version = pkgs.runCommand "hello-go-version" {
nativeBuildInputs = [ self.packages.${system}.default ];
} ''
hello-go --version || hello-go | tee out
touch $out
'';
devShells.${system}.default = pkgs.mkShell {
packages = [
pkgs.go
pkgs.gopls
self.packages.${system}.default
];
};
};
}Local module without GitHub (lab friendly):
{ lib, buildGoModule }:
buildGoModule {
pname = "hello-go";
version = "0.1.0";
src = ../src/hello-go;
vendorHash = null; # only if no external modules
meta = { mainProgram = "hello-go"; license = lib.licenses.mit; };
}If you import modules, use a real vendorHash.
Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day47
Step 1 — Choose primary language
Write the choice in NOTES.md: Go / Rust / Python / Node / other. Stick to it for all steps.
Step 2 — Minimal upstream project
Either:
- Create a tiny local project under
src/, or
- Fetch a small tagged tool you actually use.
Requirements: builds offline once vendored; one CLI entrypoint; license known.
Step 3 — First package expression
Use the matching builder. Start with lib.fakeHash / fake vendor hashes as appropriate.
nix build .#default -LIterate until green.
Step 4 — Wire meta.mainProgram and run
nix run .#
# or
./result/bin/<name> --helpStep 5 — Version injection
Pass version via ldflags (Go), env (Rust), or __version__ (Python). Prove tool --version matches derivation version.
Step 6 — Tests
Enable package checks if feasible (doCheck, cargoTest, pytestCheckHook). If tests need network, patch or disable with a comment explaining why—not silence.
Step 7 — Consume from a devShell and document host install
nix develop
which <tool>Optional: add to a NixOS environment.systemPackages via flake package output (pattern for Day 56 gate).
Step 8 — Break the vendor hash
Edit a dependency version or go.sum line; rebuild; observe hash mismatch; restore or update intentionally.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
vendorHash / cargoHash / npmDepsHash mismatch |
Update after intentional lock change; never ignore silently |
| Go build tries network in buildPhase | Modules must be vendored via FOD; check proxyVendor / vendor dir |
| Rust git dependency hash missing | Add outputHashes for the git crate |
| Python module import works in tests but CLI missing | Use buildPythonApplication + meta.mainProgram |
| Node package missing lockfile | Generate supported lock; don’t hand-wave node_modules |
mainProgram wrong |
Fix meta; nix run depends on it for default bin |
| CGO / shared libs surprise | Set CGO_ENABLED, add buildInputs for libs |
| Building entire monorepo | subPackages / cargoBuildFlags / workspace members |
Checkpoint
- Primary language chosen and recorded
- Package builds with official language helper
- Vendor/cargo/npm hash workflow demonstrated
nix runor./result/binworks
- Version string controlled from Nix where possible
- Notes on how this maps back to stdenv phases
Commit
git add .
git commit -m "day47: language ecosystem A package"Write three personal gotchas before continuing.
Tomorrow
Day 48 — Language ecosystem B: a second ecosystem for pattern transfer (smaller package).