Day 48 — Language ecosystem B

Updated

July 30, 2026

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-b

Record 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 ./result

Note 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

  1. Intentionally change a src rev or leave hash empty if your Nix warns
  2. Run build; capture the got: hash from the error
  3. Paste correct hash; rebuild green
  4. 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.