Skip to content

fix(lock,devpkg,nix): fix package parsing, flake input names, and lockfile resolve bugs - #3010

Merged
mikeland73 merged 12 commits into
mainfrom
mikeland73/fix-package-lock-bugs
Oct 25, 2026
Merged

mikeland73 merged 12 commits into
mainfrom
mikeland73/fix-package-lock-bugs

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Fixes a batch of bugs in package resolution, the lockfile, and nix helpers (internal/devpkg, internal/lock, internal/nix, and Devbox.Outdated).

  • devbox list --outdated panic (internal/devbox/packages.go:65): lockfile.Packages[pkg.Raw] was dereferenced without a nil check, so a missing devbox.lock or a package that wasn't locked yet caused a panic. Unlocked packages are now skipped, since they get locked to the latest version on the next install anyway.
  • Empty output name for flakes (internal/devpkg/package.go:162): strings.Split("", ",") returns [""], so a flake without ^outputs got an empty output name. With --outputs dev,out the outputs became ["", "dev", "out"], which produced invalid nix (input.curl.) and duplicate installables. Empty entries are now filtered out.
  • Inconsistent @ splitting (internal/devpkg/package.go CanonicalName/version/isVersioned): these split on the first @, but searcher.ParseVersionedPackage splits on the last one. nodePackages.@vue/cli@5.0 therefore got the canonical name nodePackages.. All three now use searcher.ParseVersionedPackage.
  • Duplicate flake input names (internal/devpkg/package.go FlakeInputName): non-nixpkgs GitHub inputs were named only gh-<owner>-<repo>-<rev|ref>. github:o/r/<rev>?dir=a and ?dir=b (or a different host) ended up with the same name. When dir or host is set, a short hash of the ref is now appended. Names for the common case don't change.
  • devbox update re-fetches pinned GitHub flakes (internal/lock/resolve.go lockFlake): the !refresh condition turned off the shortcut for GitHub refs that are already pinned to a rev. Update then downloaded the source and rewrote the lock entry with lastModified/narHash params. A pinned rev can't change, so the shortcut now also applies on refresh.
  • 1970 last_modified (internal/lock/resolve.go:47): when a flake had no lastModified, time.Unix(0, 0) wrote 1970-01-01T00:00:00Z to the lockfile. The field is now left empty.
  • Store paths with dashes in the version (internal/nix/storepath.go): glibc-2.39-52-bin was parsed as version 2.39 with output 52-bin. foo-0-unstable-2024-01-01 got output unstable-2024-01-01. Now only the last dash-separated component counts as the output, and only if it starts with a letter.
  • UpdateStdenv could drop the stdenv (internal/lock/lockfile.go:144): Remove() saved the lockfile without the stdenv entry before re-resolving it, so a failed resolve left the lockfile without a stdenv. The old entry is now removed only in memory and put back if resolving fails.
  • Corrupt print-dev-env cache breaks the shell for good (internal/nix/nix.go PrintDevEnv): an unmarshal error on the cache file was returned to the caller, so a truncated cache broke PrintDevEnv until someone deleted the file by hand. The bad cache is now ignored and the env is recomputed, which rewrites the cache.
  • Every search error reported as "package not found" (internal/lock/resolve.go:70): network errors and 5xx responses from /v1/resolve were all turned into ErrPackageNotFound. Only searcher.ErrNotFound maps to it now. This matches the existing v2 path.
  • %!(EXTRA <nil>) in error message (internal/nix/store.go:164): removed an extra argument that had no matching verb in the format string.
  • trusted-users = * not handled (internal/nix/config.go IsUserTrusted): a * entry now counts as trusting every user.

How was it tested?

  • New and extended unit tests. Each one was checked to fail before its fix and pass after:
    • internal/devpkg: TestFlakeOutputNames, TestVersioned, TestFlakeInputNameUnique, plus new TestCanonicalName cases.
    • internal/lock: TestFetchResolvedPackagePinnedGitHubFlake (covers the pinned-rev refresh and the empty last_modified) and TestFetchResolvedPackageSearchErrors (an httptest search server returning 404 and 500).
    • internal/nix: new TestStorePathParts cases and TestConfigIsUserTrusted/Wildcard.
  • No unit tests for the Outdated nil check, UpdateStdenv, the print-dev-env cache, or the store.go format string. Testing these needs a real nix binary, network access, or a full Devbox project.
  • go build ./..., go vet, go test for ./internal/devpkg/... ./internal/lock/... ./internal/nix/... ./internal/devbox/... ./internal/shellgen/..., go tool golangci-lint run (0 issues), and gofumpt.

🤖 Generated with Claude Code

mikeland73 and others added 12 commits October 23, 2026 15:48
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CanonicalName, version and isVersioned used the first @, which is
inconsistent with searcher.ParseVersionedPackage and breaks names like
nodePackages.@vue/cli@5.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pinned rev can't change, so refreshing only downloads the source and
rewrites the lock entry with lastModified and narHash params.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UpdateStdenv saved the lockfile without the stdenv entry before
re-resolving it, so a failed resolve left devbox.lock without one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A truncated cache file made PrintDevEnv fail with an unmarshal error
until the cache was manually deleted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Network and server errors from the search service were all reported as
"package not found".

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

claude Bot commented Oct 23, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude

claude Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 24s —— View job


Review of #3010

I read the non-test diff and checked the changes against their callers. I didn't build the code or run the tests. I found no blocking issues.

Checked and fine

  • Outdated nil check: skipping unlocked packages is correct. The next install locks them to the latest version.
  • lo.Compact on outputs: this fixes the [""] case, and an empty Outputs now gives a nil slice.
  • CanonicalName, version and isVersioned: all three now go through searcher.ParseVersionedPackage, the same parser Resolve uses. For a trailing @ (foo@), CanonicalName now returns foo@ where it used to return foo. That matches how the lock layer treats it, so I think the new behavior is right.
  • UpdateStdenv: the old entry is deleted in memory only. Add calls Resolve and only then Save, so a failure never writes a lockfile without stdenv, and the old entry is put back in memory. This works.
  • lockFlake shortcut: a GitHub ref with a pinned Rev can't change, so skipping the fetch on refresh is safe.
  • last_modified: leaving it empty when it is zero is right. Check that the LastModified field is omitempty in the lockfile struct. The tests probably already cover this.
  • Search errors: only searcher.ErrNotFound now maps to ErrPackageNotFound. The 404 and 500 tests cover both paths.
  • PrintDevEnv: after a bad cache, data and out are reset, so the recompute path starts clean.
  • trusted-users = *: handled before the user lookup, which is correct.
  • store.go: removing the extra argument is correct.

Minor notes (non-blocking)

  1. NewStorePathParts is still ambiguous when a version's last dash component starts with a letter. For example, foo-1.0-rc1 has rc1 taken as the output, and foo-0-unstable has unstable taken as the output. This is no worse than before, and nix store paths can't resolve it without extra info. A one-line comment saying so might help.
  2. FlakeInputName now hashes Ref.String() when dir or host is set. That makes the name depend on the string form of the ref. Names for the common case are unchanged, so existing locks are unaffected. Any future change to Ref.String() formatting would rename these inputs.
  3. The UpdateStdenv rollback and the PrintDevEnv cache fallback have no tests. The PR description says why, and I'm fine with that.

The changes are targeted and the tests cover the parts that can be tested without nix or network.

@mikeland73
mikeland73 merged commit f96c489 into main Oct 25, 2026
29 checks passed
@mikeland73
mikeland73 deleted the mikeland73/fix-package-lock-bugs branch October 25, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant