Day 48 — Language ecosystem B
Day 48 — Language ecosystem B
Stage V · ~4h
Goal: Package a second language ecosystem tool so the patterns transfer: FODs for language deps, helpers vs raw stdenv, and how overrides differ across builders.
Why this day exists
At work you will not stay monoglot. The point is not fluency in five builders—it is recognizing the same skeleton:
src FOD → language-dep FOD → build/install hooks → $out/bin → meta
Day 47 was depth. Today is transfer with a deliberately smaller scope.
Theory 1 — Pattern map across ecosystems
| Concern | Go | Rust | Python | Node |
|---|---|---|---|---|
| Source pin | src.hash |
src.hash |
src.hash / fetchPypi |
src.hash |
| Dep pin | vendorHash |
cargoHash / lock |
package set / frozen | npmDepsHash |
| Builder | buildGoModule |
buildRustPackage |
buildPython* |
buildNpmPackage |
| Skip tests | doCheck |
doCheck / cargo flags |
pytest hooks |
npm build only |
| Binary name | module main | crate bin | meta.mainProgram |
meta.mainProgram |
| Cross | GOOS/GOARCH care |
target triples | often pure | often pure JS |
When stuck, ask: which hash is wrong—source or language deps?
Theory 2 — Choose B deliberately
| If Day 47 was… | Good Day 48 pick | Why |
|---|---|---|
| Go | Python CLI or Rust | Different dep story |
| Rust | Go small tool | Faster vendor cycle |
| Python | Node or Go | Leaves interpreter wrapping |
| Node | Python or Go | Sees compiled vs interpreted |
Avoid starting a second multi-crate workspace today. One binary, clear version, known license.
Theory 3 — Wrapping and runtime deps
Interpreted ecosystems often need wrappers:
# Python apps usually wrap PYTHONPATH via the builder
# Manual wrap example for a shell/python hybrid:
{ stdenv, lib, python3, makeWrapper }:
stdenv.mkDerivation {
pname = "tool";
version = "0.1.0";
nativeBuildInputs = [ makeWrapper ];
# …
postInstall = ''
wrapProgram $out/bin/tool \
--prefix PATH : ${lib.makeBinPath [ python3 ]}
'';
}| Situation | Prefer |
|---|---|
| Official language builder exists | Use it |
| Need one env var or PATH sibling | wrapProgram / makeWrapper |
| Need whole tree of modules | Builder’s dependency graph |
Theory 4 — Overrides preview (deep dive Day 49)
# Change flags without forking the whole expression
helloGo.overrideAttrs (old: {
ldflags = (old.ldflags or []) ++ [ "-X main.extra=day48" ];
})# Python package override inside python3.pkgs
python3.pkgs.myPkg.overridePythonAttrs (old: {
doCheck = false;
})Today: know these exist. Tomorrow: patch and overlay composition for real.
Worked example — dual packages in one flake
{
description = "Day 48 — second ecosystem";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
packages = forAllSystems (system:
let pkgs = import nixpkgs { inherit system; }; in {
lang-a = pkgs.callPackage ./pkgs/lang-a.nix { }; # from day47, copy or input
lang-b = pkgs.callPackage ./pkgs/lang-b.nix { };
default = self.packages.${system}.lang-b;
});
checks = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
a = self.packages.${system}.lang-a;
b = self.packages.${system}.lang-b;
in {
both-run = pkgs.runCommand "both-run" {
nativeBuildInputs = [ a b ];
} ''
# invoke both CLIs smoke-test style
${a}/bin/* --help >/dev/null 2>&1 || true
${b}/bin/* --help >/dev/null 2>&1 || true
touch $out
'';
});
};
}Example B — tiny Python app (if A was compiled)
{ lib, python3Packages }:
python3Packages.buildPythonApplication {
pname = "day48-greet";
version = "0.1.0";
pyproject = true;
src = ../src/day48-greet;
build-system = with python3Packages; [ setuptools ];
meta = {
description = "Day 48 greet";
license = lib.licenses.mit;
mainProgram = "day48-greet";
};
}# src/day48-greet/pyproject.toml
[project]
name = "day48-greet"
version = "0.1.0"
requires-python = ">=3.11"
[project.scripts]
day48-greet = "day48_greet:main"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"# src/day48-greet/day48_greet.py
def main():
print("day48 ecosystem B")
if __name__ == "__main__":
main()Example B — tiny Node CLI (alternative)
{ lib, buildNpmPackage }:
buildNpmPackage {
pname = "day48-node";
version = "0.1.0";
src = ../src/day48-node;
npmDepsHash = "sha256-…";
meta = {
mainProgram = "day48-node";
license = lib.licenses.mit;
};
}Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day48
Step 1 — Import or copy Day 47 package
Keep lang-a buildable so you compare logs side by side.
Step 2 — Scaffold smaller B project
Budget: under ~50 lines of application code. No microservices.
Step 3 — Package with the B builder
nix build .#lang-b -L
nix run .#lang-bRecord every hash you had to set (src + language deps).
Step 4 — Comparison table (required)
In NOTES.md:
| Question | Lang A | Lang B |
|---|---|---|
| Builder name | ||
| Source hash field | ||
| Dep hash field | ||
| Time to first success | ||
| Hardest error | ||
| Where binary lands | ||
| Would I use this at work for CLIs? |
Step 5 — Unified check
Add a flake checks.* that runs both tools (smoke). nix flake check.
Step 6 — Optional: same interface
Make both print a comparable line (day47 / day48) so a shell script can assert either.
Step 7 — Closure glance
nix path-info -rS ./result | sort -k2 -n | tail
nix path-info -Sh ./resultNote which ecosystem pulled a heavier closure and why (interpreter? compiler runtime? libc?).
Theory 5 — Cross-ecosystem FOD discipline
Whatever language B you chose, fetchers obey the same rules as Day 46:
| Rule | Why |
|---|---|
| Pin URL + hash | Reproducible |
| Vendor offline where possible | CI without network flakiness |
| Prefer language-native lock → Nix conversion tools carefully | Don’t invent hashes by hand when tooling exists |
# conceptual — language-specific helpers differ
# buildGoModule / buildPythonPackage / rustPlatform.buildRustPackage
# each has its own cargoHash / vendorHash / etc.Document the hash field name for your ecosystem B in notes—future-you will grep for it.
Theory 6 — Runtime vs build-time closure
nativeBuildInputs → tools for build (compilers, make)
buildInputs → linked/used at build; may propagate
propagated* → infect dependents (use sparingly)
A package that “works in nix develop” but fails as nix build often mixed shell conveniences with missing buildInputs.
Worked example — dual-output mental model
Language ecosystems often ship:
- CLI binary
- library crate/wheel/module
On Nix, that may be one derivation or split outputs (Day 50). For today: ensure meta.mainProgram / bin path is clear when wrapping.
Lab — hash bump drill
- Intentionally change a src rev or leave hash empty if your Nix warns
- Run build; capture the got: hash from the error
- Paste correct hash; rebuild green
- Write the exact error class name in notes
Lab — compare A vs B one-pager
Table in notes:
| Dimension | Eco A (Day 47) | Eco B (today) |
|---|---|---|
| Builder helper | ||
| Lockfile | ||
| Hash field | ||
| Common footgun |
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Trying to “finish” a second huge app | Shrink scope; transfer patterns, not product |
| Mixing package sets (python311 vs python312) | Align interpreters |
Assuming vendorHash = null always works |
Only pure module-free trees |
npmDepsHash churn from lock format |
Commit lock; pin npm |
| Comparing wall times unfairly | Same machine, warm vs cold cache noted |
Forgetting meta.license |
Set it; habit for nixpkgs-shaped work |
Checkpoint
- Second ecosystem package builds and runs
- Comparison table filled honestly
- Both A and B reachable from one flake
- At least one combined check
- Closure notes captured
Commit
git add .
git commit -m "day48: language ecosystem B + comparison"Write three personal gotchas before continuing.
Tomorrow
Day 49 — Patching & overrides: overrideAttrs, patches, and overlay composition without forking nixpkgs.