Day 47 — Language ecosystem A

Updated

July 30, 2026

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.

Tip

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

  1. Set vendorHash = lib.fakeHash; (or null only if truly vendorless—rare for modules).
  2. Build; read got: hash.
  3. Paste; rebuild.
  4. When go.mod / go.sum change, vendorHash changes.
nix build .#hello-go -L

Useful 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 -L

Iterate until green.

Step 4 — Wire meta.mainProgram and run

nix run .#
# or
./result/bin/<name> --help

Step 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 run or ./result/bin works
  • 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).