From 1f266cfa4048d5c71b39e3f458aa5f1cfa348579 Mon Sep 17 00:00:00 2001 From: Tom van der Lee Date: Thu, 24 Sep 2026 20:22:57 +0200 Subject: Enable nix-ld so uv-managed prebuilt binaries run on NixOS uv fetches prebuilt CPython interpreters and native wheel extensions that expect a standard glibc loader path, which doesn't exist on NixOS without nix-ld. Enables it unconditionally on every NixOS host and extends its library set with common CPython native-extension dependencies not already covered by nix-ld's own defaults. Co-Authored-By: Claude Sonnet 5 --- modules/nixos/common.nix | 15 +++++++++ .../2026-09-24-enable-nix-ld/.openspec.yaml | 2 ++ .../archive/2026-09-24-enable-nix-ld/design.md | 37 ++++++++++++++++++++++ .../archive/2026-09-24-enable-nix-ld/proposal.md | 25 +++++++++++++++ .../specs/dynamic-linking-support/spec.md | 23 ++++++++++++++ .../archive/2026-09-24-enable-nix-ld/tasks.md | 13 ++++++++ openspec/specs/dynamic-linking-support/spec.md | 25 +++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 openspec/changes/archive/2026-09-24-enable-nix-ld/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-24-enable-nix-ld/design.md create mode 100644 openspec/changes/archive/2026-09-24-enable-nix-ld/proposal.md create mode 100644 openspec/changes/archive/2026-09-24-enable-nix-ld/specs/dynamic-linking-support/spec.md create mode 100644 openspec/changes/archive/2026-09-24-enable-nix-ld/tasks.md create mode 100644 openspec/specs/dynamic-linking-support/spec.md diff --git a/modules/nixos/common.nix b/modules/nixos/common.nix index 919e2f0..7a79d93 100644 --- a/modules/nixos/common.nix +++ b/modules/nixos/common.nix @@ -87,4 +87,19 @@ # tom's docker group membership is root-equivalent access, accepted here # for reliability over the rootless isolation guarantee. virtualisation.docker.enable = true; + + # Lets prebuilt, foreign dynamically-linked binaries run on NixOS - needed + # for uv-managed prebuilt Python interpreters and native wheel extensions, + # which expect a standard (non-NixOS) dynamic loader path. The libraries + # below extend nix-ld's own baseline (zlib, openssl, bzip2, etc.) with + # common CPython build dependencies it doesn't already include. + programs.nix-ld.enable = true; + programs.nix-ld.libraries = with pkgs; [ + ncurses + libffi + sqlite + readline + gdbm + expat + ]; } diff --git a/openspec/changes/archive/2026-09-24-enable-nix-ld/.openspec.yaml b/openspec/changes/archive/2026-09-24-enable-nix-ld/.openspec.yaml new file mode 100644 index 0000000..75289e4 --- /dev/null +++ b/openspec/changes/archive/2026-09-24-enable-nix-ld/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-24 diff --git a/openspec/changes/archive/2026-09-24-enable-nix-ld/design.md b/openspec/changes/archive/2026-09-24-enable-nix-ld/design.md new file mode 100644 index 0000000..aa9abff --- /dev/null +++ b/openspec/changes/archive/2026-09-24-enable-nix-ld/design.md @@ -0,0 +1,37 @@ +## Context + +See `proposal.md` - Why for motivation. Relevant constraints verified against this flake's pinned nixpkgs (`nixos-unstable`, rev `b1b875982b17dabde9b4a37f3e229e74913e6db3`), `nixos/modules/programs/nix-ld.nix`: + +- The module's real option surface is `programs.nix-ld.enable` (bool), `programs.nix-ld.package`, and `programs.nix-ld.libraries` (`listOf package`). +- `programs.nix-ld.libraries` is a list-typed option, so NixOS's module system merges every module's contribution by concatenation rather than override. +- Once `enable`d, the module's own `config` block (not its `default`) unconditionally sets `programs.nix-ld.libraries` to a baseline set: `zlib zstd stdenv.cc.cc curl openssl attr libssh bzip2 libxml2 acl libsodium util-linux xz systemd`. This baseline always applies — it is not something a consuming module can accidentally clobber by also setting the option. +- `modules/nixos/common.nix` currently has no `lib.mkIf`/feature-flag pattern anywhere; every service (SSH, GNOME, Docker, PipeWire, printing) is turned on unconditionally for every host, with `lib.mkDefault` used only for the two settings (`time.timeZone`, `i18n.defaultLocale`) that a per-host `configuration.nix` is expected to override. +- Only one real host exists today (`hosts/test-vm/`), and neither it nor `hosts/template/` sets any GPU/CUDA-related packages that would need to be folded into a `nix-ld` library list. +- `programs.nix-ld` has no equivalent in home-manager and is not read by `modules/home/*`; it is purely a NixOS system module concern, so it cannot affect the standalone `homeConfigurations` built via `mkHome`. + +## Goals / Non-Goals + +**Goals:** +- `uv`-fetched prebuilt CPython interpreters and native wheel extensions run correctly on every NixOS host built from this flake, today and for hosts added later. +- The library list is easy to extend later without restructuring anything, since new uv-fetched binaries may need libraries not anticipated now. + +**Non-Goals:** +- A full FHS-compatible environment (`buildFHSEnv`/`buildFHSUserEnv`) — `nix-ld` alone is sufficient for the driving use case (running foreign dynamically-linked binaries), and an FHS wrapper is a much larger, unrelated commitment. +- GPU/CUDA-specific libraries — no host in this flake currently has a GPU driver or CUDA toolkit configured, so there is nothing host-specific to add yet. +- A per-host opt-out or feature flag for `nix-ld` — only one real host exists, and `modules/nixos/common.nix` doesn't use feature flags for any of its other unconditional services; a future host that genuinely needs to opt out can still override with `lib.mkForce false` without requiring this design to add a flag preemptively. + +## Decisions + +**Enable `programs.nix-ld.enable` unconditionally in `modules/nixos/common.nix`, not per-host.** +Matches this file's existing style — every other service (Docker, GNOME, SSH) is unconditional, with `mkDefault` reserved specifically for settings a host is expected to override (locale/timezone). `nix-ld` has no such per-host variance today. Alternative considered: gate it behind a new `home-manager`-style option so hosts could opt in/out — rejected as unnecessary complexity with no current use case, and inconsistent with how every other service in this file is already handled. + +**Extend `programs.nix-ld.libraries` with a curated Python-native-extension list, rather than leaving it at nix-ld's own defaults.** +nix-ld's built-in baseline (`zlib`, `openssl`, `bzip2`, `libxml2`, `xz`, `stdenv.cc.cc`, etc.) covers a lot, but common CPython build dependencies it does *not* include are `ncurses`, `libffi`, `sqlite`, `readline`, `gdbm`, and `expat` — all commonly required by python-build-standalone interpreters and native extension wheels. Because the option merges additively, `common.nix` only needs to state the delta, not restate nix-ld's own defaults. Alternative considered: leave `programs.nix-ld.libraries` unset and rely solely on nix-ld's own baseline — rejected because it would likely leave real CPython interpreter/extension failures unresolved, defeating the purpose of this change. + +**Keep the library list inline in `modules/nixos/common.nix` rather than a separate `lib/nix-ld-libraries.nix` file.** +The list is short (six packages) and consumed in exactly one place, unlike `lib/unfree-allowlist.nix`, which is imported by both `mkNixos` and `mkHome`. Alternative considered: mirror the `lib/unfree-allowlist.nix` pattern with a dedicated file — rejected as premature indirection for a list this size with a single consumer; can be extracted later if it grows or gains a second consumer. + +## Risks / Trade-offs + +- **[Risk]** The curated library list is best-effort and may not cover every binary `uv` ever fetches, so some future interpreter or native extension could still fail to load a shared library. → **Mitigation**: `programs.nix-ld.libraries` merges additively (confirmed from the module source), so closing a future gap is a one-line addition to the existing list, not a restructuring. +- **[Risk]** This change cannot be fully verified from this development environment — confirming a `uv`-managed interpreter or native extension actually runs requires an activated NixOS host (`nixos-rebuild switch`), not just a successful build. → **Mitigation**: verification here is limited to `nix flake check` and building the `test-vm` system closure; end-to-end confirmation is called out as a manual follow-up in `tasks.md` rather than claimed as done. diff --git a/openspec/changes/archive/2026-09-24-enable-nix-ld/proposal.md b/openspec/changes/archive/2026-09-24-enable-nix-ld/proposal.md new file mode 100644 index 0000000..2a51e37 --- /dev/null +++ b/openspec/changes/archive/2026-09-24-enable-nix-ld/proposal.md @@ -0,0 +1,25 @@ +## Why + +`uv` (provisioned in `modules/home/dev-tools.nix`) doesn't just run Python code — `uv python install` fetches prebuilt CPython interpreters (python-build-standalone), and `uv pip install`/`uv tool install` can pull wheels with prebuilt native (C-extension) binaries. These are ordinary dynamically-linked ELF binaries built against a generic glibc loader path (`/lib64/ld-linux-x86-64.so.2`) that doesn't exist on NixOS, so they fail to execute without `nix-ld`. This was explicitly flagged as a known, deferred gap in the change that added `uv` itself (`openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/proposal.md`: *"no `nix-ld`/extra container-toolchain packages beyond what's already provisioned"*). This change closes that gap. + +## What Changes + +- Enable `programs.nix-ld.enable = true;` unconditionally on every NixOS host, in `modules/nixos/common.nix`, alongside the other unconditional host-level services (GNOME, SSH, Docker) — matching that file's existing no-feature-flag style. +- Extend `programs.nix-ld.libraries` with a small curated set of common CPython native-extension dependencies (`ncurses`, `libffi`, `sqlite`, `readline`, `gdbm`, `expat`) that aren't already covered by nix-ld's own built-in baseline list (`zlib`, `zstd`, `stdenv.cc.cc`, `curl`, `openssl`, `attr`, `libssh`, `bzip2`, `libxml2`, `acl`, `libsodium`, `util-linux`, `xz`, `systemd`, confirmed from the pinned nixpkgs module source). `programs.nix-ld.libraries` merges additively across modules, so this only adds to nix-ld's own defaults rather than replacing them. +- **BREAKING**: none — additive only. Only applies to NixOS hosts (`modules/nixos/common.nix`); the standalone `tom@linux`/`tom@darwin` home-manager profiles (`homeConfigurations`, built via `mkHome`) are unaffected, since `programs.nix-ld` is a NixOS-only module option with no equivalent in home-manager. + +## Capabilities + +### New Capabilities +- `dynamic-linking-support`: NixOS hosts built from this flake can run prebuilt, foreign (non-Nix-built) dynamically-linked binaries — such as the interpreters and native wheel extensions `uv` fetches — via `nix-ld`, rather than failing with a missing-interpreter/exec error. + +### Modified Capabilities +- None. `developer-environment` already provisions `uv` itself and makes no claim about dynamic-linking behavior, so its spec is untouched. + +## Impact + +- **Affected code**: `modules/nixos/common.nix` only (add `programs.nix-ld.enable` and extend `programs.nix-ld.libraries`). +- **Not affected**: `modules/home/*` (including `dev-tools.nix`, which already provisions `uv`), `homeConfigurations`/`lib/mkHome.nix`, `lib/unfree-allowlist.nix` (the `nix-ld` package and all added libraries are free-licensed, no allowlist entry needed), `hosts/template/` and `hosts/test-vm/` (no per-host override needed — this is a shared baseline, same as GNOME/SSH/Docker). +- **Dependencies**: nixpkgs' built-in `nix-ld` NixOS module (`nixos/modules/programs/nix-ld.nix`) and its default `nix-ld` package, both already present in the pinned `nixos-unstable` input; no new flake inputs. +- **Platforms**: NixOS hosts only (`nixosConfigurations`, via `mkNixos`/`modules/nixos/common.nix`). No effect on standalone home-manager-only platforms. +- **Known limitation, out of scope**: the curated library list is best-effort, not exhaustive — some future uv-fetched binary could still need a library not in the list. Because `programs.nix-ld.libraries` merges additively, closing such a gap later is a one-line addition, not a rework. diff --git a/openspec/changes/archive/2026-09-24-enable-nix-ld/specs/dynamic-linking-support/spec.md b/openspec/changes/archive/2026-09-24-enable-nix-ld/specs/dynamic-linking-support/spec.md new file mode 100644 index 0000000..e859aa4 --- /dev/null +++ b/openspec/changes/archive/2026-09-24-enable-nix-ld/specs/dynamic-linking-support/spec.md @@ -0,0 +1,23 @@ +## Purpose + +Lets NixOS hosts built from this flake run prebuilt, foreign (non-Nix-built) dynamically-linked binaries — such as the interpreters and native wheel extensions that developer tools like `uv` fetch at runtime — instead of failing with a missing-interpreter execution error. + +## ADDED Requirements + +### Requirement: NixOS hosts run foreign dynamically-linked binaries via nix-ld +Every NixOS host built from this flake's shared system module SHALL enable `nix-ld`, so that prebuilt binaries linked against a standard (non-NixOS) dynamic loader path execute correctly instead of failing to run. + +#### Scenario: A uv-managed prebuilt Python interpreter executes +- **WHEN** `uv python install` downloads a prebuilt CPython interpreter and that interpreter is invoked +- **THEN** the interpreter starts successfully instead of failing with a missing-interpreter or "cannot execute binary file" error + +#### Scenario: A native wheel extension loads its shared library dependencies +- **WHEN** a Python package installed by `uv` includes a prebuilt native (C-extension) binary that depends on common shared libraries (e.g. `ncurses`, `libffi`, `sqlite`, `readline`, `gdbm`, `expat`, or another library covered by `nix-ld`'s configured library set) +- **THEN** the extension loads its dependencies successfully instead of failing with a missing shared library error + +### Requirement: The nix-ld library set is host-wide and additive +The set of libraries made available to foreign binaries via `nix-ld` SHALL be defined once for all NixOS hosts built from this flake's shared system module, and SHALL be extensible by appending further libraries without removing or replacing the existing set. + +#### Scenario: A host-level addition does not remove existing libraries +- **WHEN** a NixOS host's own configuration adds an additional library to the nix-ld library set +- **THEN** the libraries already provided by the shared system module remain available, unaffected by the addition diff --git a/openspec/changes/archive/2026-09-24-enable-nix-ld/tasks.md b/openspec/changes/archive/2026-09-24-enable-nix-ld/tasks.md new file mode 100644 index 0000000..fdd8abe --- /dev/null +++ b/openspec/changes/archive/2026-09-24-enable-nix-ld/tasks.md @@ -0,0 +1,13 @@ +## 1. Enable nix-ld on NixOS hosts + +- [x] 1.1 In `modules/nixos/common.nix`, add `programs.nix-ld.enable = true;` alongside the other unconditional host-level services, with a short comment explaining why (uv-managed prebuilt binaries need an FHS-style loader path). Verify by re-reading the diff: the option is set unconditionally, matching the file's existing style. +- [x] 1.2 In the same file, add `programs.nix-ld.libraries = with pkgs; [ ncurses libffi sqlite readline gdbm expat ];`. Verify the six packages resolve in the pinned nixpkgs by running `nix eval '.#nixosConfigurations.test-vm.config.programs.nix-ld.libraries' --apply 'builtins.length'` (or equivalent) and confirming it evaluates without error. + +## 2. Verify the change builds + +- [x] 2.1 Run `nix flake check` and confirm it passes. +- [x] 2.2 Run `nix build '.#nixosConfigurations.test-vm.config.system.build.toplevel'` and confirm the system closure builds successfully with `programs.nix-ld` enabled. + +## 3. Manual follow-up (deferred, not verifiable in this environment) + +- [ ] 3.1 After deploying to an activated NixOS host, run `uv python install 3.12` and confirm the installed interpreter executes (e.g. `uv run python3.12 --version`) instead of failing with a missing-interpreter error. This cannot be verified from this development environment since it requires an activated host, not just a successful build. diff --git a/openspec/specs/dynamic-linking-support/spec.md b/openspec/specs/dynamic-linking-support/spec.md new file mode 100644 index 0000000..5bc7fd9 --- /dev/null +++ b/openspec/specs/dynamic-linking-support/spec.md @@ -0,0 +1,25 @@ +# dynamic-linking-support Specification + +## Purpose + +Lets NixOS hosts built from this flake run prebuilt, foreign (non-Nix-built) dynamically-linked binaries — such as the interpreters and native wheel extensions that developer tools like `uv` fetch at runtime — instead of failing with a missing-interpreter execution error. + +## Requirements + +### Requirement: NixOS hosts run foreign dynamically-linked binaries via nix-ld +Every NixOS host built from this flake's shared system module SHALL enable `nix-ld`, so that prebuilt binaries linked against a standard (non-NixOS) dynamic loader path execute correctly instead of failing to run. + +#### Scenario: A uv-managed prebuilt Python interpreter executes +- **WHEN** `uv python install` downloads a prebuilt CPython interpreter and that interpreter is invoked +- **THEN** the interpreter starts successfully instead of failing with a missing-interpreter or "cannot execute binary file" error + +#### Scenario: A native wheel extension loads its shared library dependencies +- **WHEN** a Python package installed by `uv` includes a prebuilt native (C-extension) binary that depends on common shared libraries (e.g. `ncurses`, `libffi`, `sqlite`, `readline`, `gdbm`, `expat`, or another library covered by `nix-ld`'s configured library set) +- **THEN** the extension loads its dependencies successfully instead of failing with a missing shared library error + +### Requirement: The nix-ld library set is host-wide and additive +The set of libraries made available to foreign binaries via `nix-ld` SHALL be defined once for all NixOS hosts built from this flake's shared system module, and SHALL be extensible by appending further libraries without removing or replacing the existing set. + +#### Scenario: A host-level addition does not remove existing libraries +- **WHEN** a NixOS host's own configuration adds an additional library to the nix-ld library set +- **THEN** the libraries already provided by the shared system module remain available, unaffected by the addition -- cgit v1.3