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

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

numa's default system-wide data directory was hardcoded to
/usr/local/var/numa for all Unix platforms. This is the right path on
macOS (Homebrew prefix convention) but non-FHS on Linux, where Arch /
Fedora / Debian / etc. expect persistent state under /var/lib/<pkg>.
The mismatch was invisible to existing users (numa creates the dir
silently on first run) but immediately surfaces when packaging for a
distro — see PR #33 (community contribution to add an Arch AUR package)
which had to add fragile sed-based path patching at PKGBUILD build time.

The fix moves the path decision into a small helper:

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

Linux behavior:
  - Fresh install                       → /var/lib/numa (FHS)
  - Upgrading from pre-v0.10.1 install  → /usr/local/var/numa (legacy)
  - Both paths exist                    → /var/lib/numa (FHS wins)

The legacy fallback is critical: existing v0.10.0 Linux users have
their CA cert + services.json under /usr/local/var/numa. Returning
the new path unconditionally would cause CA regeneration on upgrade,
breaking every browser that had trusted the previous CA. The fallback
is checked at startup via std::path::Path::exists, so the upgrade is
seamless and zero-config.

macOS behavior is unchanged — /usr/local/var/numa is still correct
because Homebrew's prefix is /usr/local.

Test coverage:

  - resolve_linux_data_dir is a pure function gated cfg(any(linux,test))
    so the same code path is unit-tested on every platform's CI run.
  - Four tests cover all combinations of (legacy_exists, fhs_exists),
    asserting the migration logic stays correct under future edits.

The default config in numa.toml is also updated to document the new
per-platform default paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: end-to-end FHS path verification + simplify cleanup

Two related changes from a /simplify pass and a follow-up testing
finalization:

1. lib.rs cleanup (no behavior change):
   - Drop FHS_LINUX_DATA_DIR and LEGACY_LINUX_DATA_DIR consts. Both
     were used in only 4 places total and the unit tests already
     bypassed them with string literals, so they were over-engineering.
     Inline the strings in daemon_data_dir() and resolve_linux_data_dir().
   - Trim narrating doc/comments on the helper and the test bodies.
     Keep only the non-obvious WHY (the macOS Homebrew note and the
     migration-keeps-legacy rationale).

2. tests/docker/smoke-arch.sh:
   - Cherry-picked the previously-uncommitted Arch compatibility smoke
     test from feat/smoke-arch.
   - Removed the [server] data_dir = "/tmp/numa-smoke" override from
     the test config so the script now exercises the DEFAULT data dir
     code path — which is exactly what the FHS fix touches.
   - Added a path assertion after the dig succeeds: verify that
     /var/lib/numa/ca.pem exists (FHS) and /usr/local/var/numa is
     absent (no accidental dual-creation on a fresh install).

Verified end-to-end on archlinux:latest (Apple Silicon, Rosetta):

  ── building + running numa on archlinux:latest ──
  ── cargo build --release --locked ──
      Finished `release` profile [optimized] target(s) in 24.02s
  ── dig @127.0.0.1 -p 5354 google.com A ──
    142.251.38.206
  ── FHS path check ──
    ✓ CA cert at /var/lib/numa/ca.pem (FHS path)
    ✓ legacy path /usr/local/var/numa absent (fresh install used FHS)
  ── smoke-arch passed ──

This closes the testing gap where the unit tests covered the
path-decision LOGIC in isolation but nothing exercised the live
wiring on a real Linux filesystem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #43.
This commit is contained in:
Razvan Dimescu
2026-04-08 18:00:27 +03:00
committed by GitHub
parent bf5565ac26
commit 79ecb73d87
3 changed files with 217 additions and 8 deletions

View File

@@ -26,7 +26,10 @@ pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>;
/// Shared config directory for persistent data (services.json, etc).
/// Unix: ~/.config/numa/ (or /usr/local/var/numa/ when running as root daemon)
/// Unix users: ~/.config/numa/
/// Linux root daemon: /var/lib/numa (FHS) — falls back to /usr/local/var/numa
/// if a pre-v0.10.1 install already lives there.
/// macOS root daemon: /usr/local/var/numa (Homebrew prefix)
/// Windows: %APPDATA%\numa
pub fn config_dir() -> std::path::PathBuf {
#[cfg(windows)]
@@ -63,13 +66,15 @@ fn config_dir_unix() -> std::path::PathBuf {
}
// Running as root daemon (launchd/systemd) — use system-wide path
std::path::PathBuf::from("/usr/local/var/numa")
daemon_data_dir()
}
/// Default system-wide data directory for TLS certs. Overridable via
/// `[server] data_dir = "..."` in numa.toml — this function only provides
/// the fallback when the config doesn't set it.
/// Unix: /usr/local/var/numa
/// Linux: /var/lib/numa (FHS) — falls back to /usr/local/var/numa if a
/// pre-v0.10.1 install already has data there.
/// macOS: /usr/local/var/numa (Homebrew prefix)
/// Windows: %PROGRAMDATA%\numa
pub fn data_dir() -> std::path::PathBuf {
#[cfg(windows)]
@@ -81,6 +86,62 @@ pub fn data_dir() -> std::path::PathBuf {
}
#[cfg(not(windows))]
{
daemon_data_dir()
}
}
/// Resolve the system-wide data directory for the running platform.
/// Honors backwards compatibility with pre-v0.10.1 installs that still
/// have their CA cert + services.json under `/usr/local/var/numa`.
#[cfg(not(windows))]
fn daemon_data_dir() -> std::path::PathBuf {
#[cfg(target_os = "linux")]
{
std::path::PathBuf::from(resolve_linux_data_dir(
std::path::Path::new("/usr/local/var/numa").exists(),
std::path::Path::new("/var/lib/numa").exists(),
))
}
#[cfg(target_os = "macos")]
{
// macOS uses the Homebrew prefix convention; no FHS migration needed.
std::path::PathBuf::from("/usr/local/var/numa")
}
}
/// Extracted as a pure function so the migration logic is unit-testable
/// without touching the real filesystem.
#[cfg(any(target_os = "linux", test))]
fn resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool) -> &'static str {
if legacy_exists && !fhs_exists {
"/usr/local/var/numa"
} else {
"/var/lib/numa"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linux_data_dir_fresh_install_uses_fhs() {
assert_eq!(resolve_linux_data_dir(false, false), "/var/lib/numa");
}
#[test]
fn linux_data_dir_upgrading_install_keeps_legacy() {
// Migration must keep legacy so the user doesn't lose their CA on upgrade.
assert_eq!(resolve_linux_data_dir(true, false), "/usr/local/var/numa");
}
#[test]
fn linux_data_dir_after_migration_uses_fhs() {
assert_eq!(resolve_linux_data_dir(true, true), "/var/lib/numa");
}
#[test]
fn linux_data_dir_only_fhs_uses_fhs() {
assert_eq!(resolve_linux_data_dir(false, true), "/var/lib/numa");
}
}