Skip to content

perf(vercheck): switch devbox versions before startup for devbox_version auto - #3023

Merged
mikeland73 merged 2 commits into
mainfrom
mikeland73/perf-early-version-switch
Oct 26, 2026
Merged

mikeland73 merged 2 commits into
mainfrom
mikeland73/perf-early-version-switch

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Implements item 7 of specs/perf-improvements.md. With devbox_version: {"version": "X", "on_mismatch": "auto"}, devbox only noticed the mismatch inside devbox.Open, after the command's PreRunE hooks (nix install check, etc.) had run. Then it re-exec'd through the bash launcher. So every invocation in a pinned project paid for two devbox startups plus the launcher.

Design

  • Early middleware (midcobra.ProjectVersion, registered first in Execute): finds the subcommand, reads only its --config/-c flag (default DEVBOX_CONFIG) into a separate pflag.FlagSet so cobra's later parse isn't affected, loads devbox.json with devconfig.Find/Open, and calls vercheck.SwitchProjectVersion. It skips devbox global ... (its config path is set in a PreRunE), --all-projects, and commands without --config. Those cases still switch from devbox.Open as before.
  • vercheck.SwitchProjectVersion re-execs only when CheckProjectVersion would. In every other case (warn/error policy, DEVBOX_USE_VERSION set by the user, no launcher, already switched) it returns without printing anything, so CheckProjectVersion still reports the same warnings and errors when the project is opened. The policy, satisfaction, and user-version checks are shared helpers.
  • Direct exec: when the launcher has already downloaded the target version ($HOME/.cache/devbox/bin/<version>_<os>_<arch>/devbox, which matches INSTALL_DIR in launch.sh), we exec that binary directly. The LAUNCHER_* and DEVBOX_LATEST_VERSION env vars are inherited. If it hasn't been downloaded, we fall back to the launcher, which downloads it. The existing __DEVBOX_AUTO_VERSION guard still prevents exec loops. This also applies when the switch happens from devbox.Open.

Deviation from the spec

The spec also suggests caching the resolved version. I didn't add a cache because auto requires an exact version (devbox.json validation rejects auto with a range), so there's nothing to resolve. The decision comes from parsing devbox.json and one semver compare, both well under a millisecond. The expensive part was the launcher, which direct exec now skips.

Testing

  • go test ./internal/vercheck/... ./internal/boxcli/... ./internal/devbox/... passes.
  • New unit tests:
    • SwitchProjectVersion: switches, and doesn't switch (and doesn't suppress the warning) in each skip case.
    • Direct exec of an installed binary, and launcher fallback when the binary isn't executable. Tests now use a temp HOME.
    • projectConfigPath: flag forms, unknown flags, -- handling, global, --all-projects, commands without --config, and a check that the command's real flags aren't touched.
  • Manual: built a binary stamped as 0.17.2 and ran shell --pure -c <proj> with devbox_version 9.9.9/auto and a fake LAUNCHER_PATH. It re-exec'd before any nix check with DEVBOX_USE_VERSION=9.9.9.
  • golangci-lint reports no issues on the changed packages.

🤖 Generated with Claude Code

…ion auto

With devbox_version on_mismatch "auto", devbox only noticed the mismatch
inside devbox.Open, after the command's PreRunE hooks (nix install check,
telemetry setup, etc.), and then re-exec'd through the bash launcher. Every
invocation paid for two devbox startups plus the launcher.

- Add a ProjectVersion middleware that runs first. It resolves the
  subcommand, reads its --config flag (or DEVBOX_CONFIG) into a separate flag
  set, loads devbox.json, and calls vercheck.SwitchProjectVersion. It skips
  devbox global, --all-projects, and commands without --config; those still
  switch when they open the project.
- SwitchProjectVersion only re-execs when CheckProjectVersion would, and
  otherwise stays silent so CheckProjectVersion still reports warnings and
  errors.
- When the launcher already downloaded the target version
  (~/.cache/devbox/bin/<version>_<os>_<arch>/devbox), exec it directly
  instead of going through the launcher. Otherwise fall back to the
  launcher, which downloads it.

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

claude Bot commented Oct 25, 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 ptal

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

claude Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

PR #3023 was updated after the trigger (2026-10-25T21:46:39Z >= 2026-10-25T21:44:37Z). Mention Claude again to run on the latest version.

@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude ptal

@claude

claude Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

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


Review of #3023

I read the full diff of the new middleware, vercheck/project.go and the root.go wiring. I did not run the tests or the linter in this pass. I found no blocking issues. These are the things I'd look at before merging, most important first.

  • Read the diff and the middleware flow in midcobra.Execute
  • Post the review

Looks right

  • ProjectVersion() is registered first in Execute, so it runs before the other middlewares and before cobra's PreRunE hooks. That is the point of the change.
  • projectConfigPath parses --config and --all-projects into a separate FlagSet. Cobra's own parse is unaffected, and the tests check this.
  • SwitchProjectVersion returns silently in every case where CheckProjectVersion would warn or error. The warning and error paths therefore stay in one place. Moving the policy, satisfaction and user-version checks into shared helpers keeps the two functions from drifting apart.
  • The __DEVBOX_AUTO_VERSION guard still prevents exec loops when the direct binary path is used.

Things to consider

  1. installedBinary and the launcher cache path (internal/vercheck/project.go). The comment says the path must match INSTALL_DIR in launch.sh, but that file isn't in this repo, so I couldn't check it. If the launcher changes its cache layout, such as honoring XDG_CACHE_HOME or adding a checksum or version-stamp step, the direct path would quietly stop being used. Behavior would still be correct, because it falls back to the launcher, but the speedup would be lost. A debug log when the binary isn't found would make this easy to diagnose.
  2. The direct binary skips whatever else the launcher does. The PR says the LAUNCHER_* and DEVBOX_LATEST_VERSION env vars are inherited, which covers what I'd expect. Please confirm the launcher sets nothing else in the exec environment that the target binary relies on, for example variables used by its update check.
  3. Informational flags. The middleware runs for --help and similar. devbox shell --help in a pinned auto project now re-execs into the pinned version first. That is probably fine and arguably desirable, since the help output matches the pinned version. It is a small behavior change you may want to mention in the PR description.
  4. Redundant config load. In a pinned project that is already on the right version, devbox.json is parsed once by the middleware and again in devbox.Open. The PR argues this is well under a millisecond, which sounds right. If it ever shows up in profiles, the middleware could pass the loaded config along.
  5. Error handling. _ = switchVersion(io.Discard, ...) deliberately drops the error, because CheckProjectVersion reports it later. That is correct, but one line at the call site saying a failed exec falls through to the normal flow would help future readers.

The spec deviation, skipping the version cache because auto requires an exact version, makes sense to me.

--- · branch mikeland73/perf-early-version-switch

@mikeland73
mikeland73 merged commit 82cc09e into main Oct 26, 2026
28 checks passed
@mikeland73
mikeland73 deleted the mikeland73/perf-early-version-switch branch October 26, 2026 00:42
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