fix: use FHS-compliant /var/lib/numa as Linux data dir default #43

Merged
razvandimescu merged 2 commits from fix/linux-fhs-paths into main 2026-04-08 23:00:28 +08:00
razvandimescu commented 2026-04-08 22:41:04 +08:00 (Migrated from github.com)

Why

numa hardcodes /usr/local/var/numa as the default system-wide data directory for all Unix platforms. This is the right path on macOS (Homebrew prefix convention) but non-FHS on Linux, where Arch / Fedora / Debian / SUSE expect persistent state under /var/lib/<pkg>.

The mismatch was invisible to existing users (numa silently creates the directory on first run) but became sharp the moment community packaging arrived: #33 had to add fragile sed-based path-patching at PKGBUILD build time, and even that patching was incomplete (only matched a stale comment).

How

A small helper extraction in src/lib.rs:

daemon_data_dir()         — cfg-gated platform dispatch (linux/macos)
resolve_linux_data_dir()  — pure function, takes "does X exist?" booleans
                            as parameters, returns the right path string

Both data_dir() and config_dir_unix() route through daemon_data_dir() so there's a single source of truth.

Linux behavior table

legacy exists fhs exists Returns Scenario
no no /var/lib/numa fresh install
yes no /usr/local/var/numa upgrading from v0.10.0 — preserves user data
yes yes /var/lib/numa post-migration
no yes /var/lib/numa clean FHS state

Migration safety

The legacy fallback is the critical row above. Existing v0.10.0 Linux users have their CA cert + services.json under /usr/local/var/numa. If we returned the new path unconditionally, every browser that had trusted the previous CA would start throwing cert errors after the upgrade because numa would generate a fresh CA at the new location.

The fallback is checked at startup via std::path::Path::exists — zero-config, no migration command needed. Users can manually move the data to /var/lib/numa later if they want; the helper handles both states.

macOS behavior

Unchanged. /usr/local/var/numa is still correct because Homebrew's prefix is /usr/local.

Tests

The path-decision logic is extracted as a pure function resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool) gated cfg(any(target_os = "linux", test)) so the same code path is unit-tested on every platform's CI run, regardless of the host OS.

Four tests cover all combinations:

linux_data_dir_fresh_install_uses_fhs       // (false, false) → /var/lib/numa
linux_data_dir_upgrading_install_keeps_legacy // (true, false) → /usr/local/var/numa
linux_data_dir_after_migration_uses_fhs     // (true, true)  → /var/lib/numa
linux_data_dir_only_fhs_uses_fhs            // (false, true) → /var/lib/numa

Plus a new end-to-end integration test in tests/docker/smoke-arch.sh that builds numa from source inside archlinux:latest, runs it with NO [server] data_dir override (so the production code path fires), and asserts:

  • CA cert lands at /var/lib/numa/ca.pem (FHS)
  • Legacy path /usr/local/var/numa doesn't exist (no accidental dual-creation)

Why this exists at all (retrospective)

The tests/docker/install-trust.sh contract test added in #41 doesn't cover this — it tests the shell command contract (does update-ca-trust actually put the cert in the system bundle?) and doesn't run numa code at all. Convention bugs like "the data directory isn't where Linux packagers expect it" are invisible to behavior-focused tests because numa works either way; the path is just non-idiomatic.

PR #33 itself was effectively the test that surfaced this — a community contributor (@CaseyLabs) packaging numa for Arch noticed the convention mismatch and submitted a fix. We didn't catch it ourselves because the dogfood machine is macOS, where the paths are correct.

Test plan

  • cargo fmt --check clean
  • cargo clippy --lib clean
  • cargo test --lib134/134 pass locally (4 new path-decision tests included)
  • CI green across Linux, macOS, Windows — cargo build + cargo test + cargo clippy -- -D warnings on every platform's real toolchain. The cfg(any(target_os = "linux", test)) gate makes the four new tests run on macOS and Windows hosts too, not just Linux.
  • End-to-end on Arch via tests/docker/smoke-arch.sh: fresh archlinux:latest container, builds numa from source, starts it without a data_dir override (exercises the live wiring), confirms /var/lib/numa/ca.pem exists and /usr/local/var/numa is absent. Verified locally on Apple Silicon (Rosetta + named cargo cache, ~1m 9s warm). Output:
    ── FHS path check ──
      ✓ CA cert at /var/lib/numa/ca.pem (FHS path)
      ✓ legacy path /usr/local/var/numa absent (fresh install used FHS)
    
  • Upgrade-from-legacy path covered by unit test linux_data_dir_upgrading_install_keeps_legacy. Integration test for the upgrade scenario (pre-create /usr/local/var/numa/, start numa, confirm legacy path is preserved) deferred — the underlying conditional is a single if legacy_exists && !fhs_exists statement, fully unit-tested. Acceptable risk per the deferral pattern used in #41 (Windows behavioral test) and #42 (no automated test for shell-call substitution).

What this unlocks

Once shipped in v0.10.1, PR #33 (Arch AUR packaging) becomes much simpler:

  • No need to sed-patch /usr/local/var/numa paths in prepare() — numa already does the right thing on Linux
  • The PR's existing path-patching attempts are unnecessary going forward
  • Other distro packagers (Fedora COPR, Debian PPA) get FHS compliance for free without per-distro patching

Closes the convention gap between numa's defaults and Linux packaging norms.

🤖 Generated with Claude Code

## Why `numa` hardcodes `/usr/local/var/numa` as the default system-wide data directory for **all** Unix platforms. This is the right path on macOS (Homebrew prefix convention) but **non-FHS on Linux**, where Arch / Fedora / Debian / SUSE expect persistent state under `/var/lib/<pkg>`. The mismatch was invisible to existing users (numa silently creates the directory on first run) but became sharp the moment community packaging arrived: #33 had to add fragile sed-based path-patching at PKGBUILD build time, and even that patching was incomplete (only matched a stale comment). ## How A small helper extraction in `src/lib.rs`: ``` daemon_data_dir() — cfg-gated platform dispatch (linux/macos) resolve_linux_data_dir() — pure function, takes "does X exist?" booleans as parameters, returns the right path string ``` Both `data_dir()` and `config_dir_unix()` route through `daemon_data_dir()` so there's a single source of truth. ### Linux behavior table | `legacy` exists | `fhs` exists | Returns | Scenario | |---|---|---|---| | no | no | `/var/lib/numa` | fresh install | | **yes** | no | `/usr/local/var/numa` | **upgrading from v0.10.0 — preserves user data** | | yes | yes | `/var/lib/numa` | post-migration | | no | yes | `/var/lib/numa` | clean FHS state | ### Migration safety The legacy fallback is the critical row above. Existing v0.10.0 Linux users have their CA cert + `services.json` under `/usr/local/var/numa`. If we returned the new path unconditionally, every browser that had trusted the previous CA would start throwing cert errors after the upgrade because numa would generate a fresh CA at the new location. The fallback is checked at startup via `std::path::Path::exists` — zero-config, no migration command needed. Users can manually move the data to `/var/lib/numa` later if they want; the helper handles both states. ### macOS behavior Unchanged. `/usr/local/var/numa` is still correct because Homebrew's prefix is `/usr/local`. ## Tests The path-decision logic is extracted as a **pure function** `resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool)` gated `cfg(any(target_os = "linux", test))` so the same code path is unit-tested on every platform's CI run, regardless of the host OS. Four tests cover all combinations: ```rust linux_data_dir_fresh_install_uses_fhs // (false, false) → /var/lib/numa linux_data_dir_upgrading_install_keeps_legacy // (true, false) → /usr/local/var/numa linux_data_dir_after_migration_uses_fhs // (true, true) → /var/lib/numa linux_data_dir_only_fhs_uses_fhs // (false, true) → /var/lib/numa ``` Plus a new end-to-end integration test in `tests/docker/smoke-arch.sh` that builds numa from source inside `archlinux:latest`, runs it with NO `[server] data_dir` override (so the production code path fires), and asserts: - CA cert lands at `/var/lib/numa/ca.pem` (FHS) - Legacy path `/usr/local/var/numa` doesn't exist (no accidental dual-creation) ## Why this exists at all (retrospective) The `tests/docker/install-trust.sh` contract test added in #41 doesn't cover this — it tests the **shell command contract** (does `update-ca-trust` actually put the cert in the system bundle?) and doesn't run numa code at all. Convention bugs like "the data directory isn't where Linux packagers expect it" are invisible to behavior-focused tests because numa works either way; the path is just non-idiomatic. PR #33 itself was effectively the test that surfaced this — a community contributor (@CaseyLabs) packaging numa for Arch noticed the convention mismatch and submitted a fix. We didn't catch it ourselves because the dogfood machine is macOS, where the paths are correct. ## Test plan - [x] `cargo fmt --check` clean - [x] `cargo clippy --lib` clean - [x] `cargo test --lib` — **134/134 pass** locally (4 new path-decision tests included) - [x] CI green across Linux, macOS, Windows — `cargo build` + `cargo test` + `cargo clippy -- -D warnings` on every platform's real toolchain. The `cfg(any(target_os = "linux", test))` gate makes the four new tests run on macOS and Windows hosts too, not just Linux. - [x] **End-to-end on Arch via `tests/docker/smoke-arch.sh`**: fresh `archlinux:latest` container, builds numa from source, starts it without a `data_dir` override (exercises the live wiring), confirms `/var/lib/numa/ca.pem` exists and `/usr/local/var/numa` is absent. Verified locally on Apple Silicon (Rosetta + named cargo cache, ~1m 9s warm). Output: ``` ── FHS path check ── ✓ CA cert at /var/lib/numa/ca.pem (FHS path) ✓ legacy path /usr/local/var/numa absent (fresh install used FHS) ``` - [x] **Upgrade-from-legacy path** covered by unit test `linux_data_dir_upgrading_install_keeps_legacy`. Integration test for the upgrade scenario (pre-create `/usr/local/var/numa/`, start numa, confirm legacy path is preserved) deferred — the underlying conditional is a single `if legacy_exists && !fhs_exists` statement, fully unit-tested. Acceptable risk per the deferral pattern used in #41 (Windows behavioral test) and #42 (no automated test for shell-call substitution). ## What this unlocks Once shipped in v0.10.1, PR #33 (Arch AUR packaging) becomes much simpler: - No need to sed-patch `/usr/local/var/numa` paths in `prepare()` — numa already does the right thing on Linux - The PR's existing path-patching attempts are unnecessary going forward - Other distro packagers (Fedora COPR, Debian PPA) get FHS compliance for free without per-distro patching Closes the convention gap between numa's defaults and Linux packaging norms. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.