Skip to content

fix(nix): don't report a broken install for a freshly mounted /nix volume (#2601) - #2939

Merged
mikeland73 merged 1 commit into
mainfrom
claude/focused-goldberg-601ixv
Sep 15, 2026
Merged

mikeland73 merged 1 commit into
mainfrom
claude/focused-goldberg-601ixv

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2601.

When running Devbox in a container (e.g. Docker or Kubernetes) with /nix mounted as a persistent volume, the volume starts out effectively empty. Devbox is supposed to install Nix into it, but instead it fails with:

Error: We found a /nix directory but nix binary is not in your PATH and we were not able to find it in the usual locations. Your nix installation might be broken. [...]

Root cause

EnsureNixInstalled decided whether /nix already contained an installation by checking whether the directory was non-empty:

if dirExistsAndIsNotEmpty("/nix") { ... return "broken installation" error ... }

A truly empty /nix already falls through to the installer, but a freshly mounted volume is usually not truly empty: ext4 (and other filesystems) create a lost+found directory at the root of the mount. So /nix looks non-empty, Devbox assumes a pre-existing (broken) install, and refuses to install Nix.

Fix

Detect an existing installation by ignoring known filesystem cruft (lost+found, .DS_Store) rather than treating any non-empty /nix as installed. A /nix that contains only those entries is now treated as a fresh install target, so Devbox proceeds to install Nix as expected. A /nix that contains real content (e.g. a store directory) is still treated as an existing installation, preserving the original "broken install" diagnostics.

The generic dirExistsAndIsNotEmpty helper was only used here, so it's replaced by a /nix-specific nixDirIsInstalled helper.

How was it tested?

  • go test ./internal/nix/ -run TestNixDirIsInstalled -v — new table-driven test covering: empty dir, dir with files, dir with a nix store, dir with hidden files, dir with only lost+found (the regression case → treated as not installed), dir with lost+found + store (→ installed), and non-existent dir. All pass.
  • go build ./... / go vet ./internal/nix/ / gofmt clean.

(Pre-existing TestConfigIsUserTrusted failures in the package are unrelated — they require a nix binary that isn't present in the CI sandbox and are not touched by this change.)

cc @ascknx (issue reporter)


Generated by Claude Code

Mounting an empty /nix volume in Docker/Kubernetes commonly leaves a
lost+found directory at the mount root (created by ext4 and other
filesystems). Devbox's install check treated any non-empty /nix as an
existing installation, so it reported a broken Nix installation instead
of installing Nix into the fresh volume.

Detect an existing installation by ignoring known filesystem cruft
(lost+found, .DS_Store) so a freshly mounted /nix volume proceeds to the
Nix installer.

Fixes #2601

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upz2KchxkA6erYatxPzVjg
@savil
savil marked this pull request as draft September 2, 2026 22:13
@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR for correctness, and flag anything that would block merging.

@claude

claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 1m 34s —— View job


Review of PR #2939: fix(nix): don't report a broken install for a freshly mounted /nix volume

  • Gather context (diff, changed files)
  • Understand the request (correctness review, blockers for merge)
  • Review internal/nix/install.go changes
  • Review internal/nix/install_test.go changes
  • Attempt to run build/vet/tests locally
  • Post final review feedback

Note: go build/go vet/go test were blocked by sandbox approval in this environment (network/tool access), so I verified correctness by manual code reading rather than execution. If you'd like me to actually run the test suite, allow go build/go test/go vet in --allowedTools.

Summary

The fix is correct and well-targeted. nixDirIsInstalled (internal/nix/install.go:43-54) replaces the old dirExistsAndIsNotEmpty (which used fileutil.IsDirEmpty) and preserves the original error semantics: os.ReadDir failing (e.g. /nix doesn't exist) still returns false → falls through to the installer, same as before. The new logic just adds one refinement: entries matching nonNixDirEntries (lost+found, .DS_Store) are skipped when deciding "installed," so a freshly mounted volume containing only lost+found is correctly treated as uninstalled, while a store dir (or any other real content) still trips the "broken installation" error path — matching the PR description exactly.

Confirmed:

  • dirExistsAndIsNotEmpty had no other call sites (internal/nix/install.go was the only user), so removing it is safe. fileutil.IsDirEmpty itself is now unused outside its own package/tests — not a blocker, just a minor future cleanup opportunity if you want to trim it (it's an exported helper, so may be intentionally kept as public API).
  • The one caller of nixDirIsInstalled (EnsureNixInstalled, internal/nix/install.go:84) is only reached when BinaryInstalled() is already false, so the marginal cost of os.ReadDir reading/sorting all entries (vs. the old early-exit IsDirEmpty) is not a practical concern — this only runs once, off the hot path.
  • Test coverage (internal/nix/install_test.go) is thorough: empty dir, populated dir, store dir, hidden file, lost+found-only (the regression case), lost+found + store, and non-existent dir. Table-driven and easy to extend.
  • Comments on nonNixDirEntries and nixDirIsInstalled clearly explain the "why" (container/VM volume mounts, filesystem cruft) and link to the originating issue — good practice for a non-obvious fix like this.

No correctness issues, no regressions, no blockers for merging. Nice, minimal fix with good regression test coverage.

Copy link
Copy Markdown
Collaborator Author

Reviewed for correctness and merge-readiness.

Correctness — looks good ✅

The change swaps the generic dirExistsAndIsNotEmpty("/nix") check for a /nix-specific nixDirIsInstalled that ignores known filesystem cruft (lost+found, .DS_Store). I checked behavior parity across every path in EnsureNixInstalled:

/nix contents old behavior new behavior
missing / empty install install (unchanged)
real content (store, var, …) "broken install" error "broken install" error (unchanged)
unreadable (permission error) install¹ install¹ (unchanged)
only lost+found (mounted volume) "broken install" error install (fixed)

¹ os.ReadDir erroring returns false here, mirroring the old fileutil.IsDirEmpty error path — so no regression for root-owned /nix read by a non-root user.

The only intended behavior change is the last row, which is exactly #2601. go build ./..., go vet, gofmt, and the new table-driven TestNixDirIsInstalled all pass locally. The now-unused dirExistsAndIsNotEmpty helper and its fileutil import are cleanly removed (no other references).

Blocking merge 🚧

  1. The PR is in draft (converted by @savil) — GitHub won't merge a draft, and the conversion dropped any auto-merge/merge-queue membership. It needs to be marked Ready for review by a maintainer. I'm intentionally not flipping it back myself.
  2. CI + approval — status is currently blocked / checks pending. Needs CI green and a maintainer approval under branch protection.

Non-blocking notes

  • This fixes Devbox's own premature guard. Whether the underlying Nix installer then installs cleanly into a mounted /nix that contains a lost+found is a separate concern outside this function — worth an end-to-end check in a real Docker volume before closing the issue, but it applied equally to the pre-existing empty-/nix install path, so it's not a regression from this PR.
  • .DS_Store is included defensively (harmless on macOS); happy to drop it if you'd prefer to scope the ignore-list strictly to lost+found.

Generated by Claude Code

@mikeland73 mikeland73 added the ready for human review Automated review passed; needs a maintainer to review and merge label Sep 14, 2026
@mikeland73
mikeland73 marked this pull request as ready for review September 14, 2026 16:19
@mikeland73
mikeland73 merged commit 3dcaa9e into main Sep 15, 2026
27 checks passed
@mikeland73
mikeland73 deleted the claude/focused-goldberg-601ixv branch September 15, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for human review Automated review passed; needs a maintainer to review and merge

Development

Successfully merging this pull request may close these issues.

Devbox is failing when an empty /nix folder exists

2 participants