bunfig: load global ~/.bunfig.toml for runtime commands; fall back past $XDG_CONFIG_HOME - #34987
bunfig: load global ~/.bunfig.toml for runtime commands; fall back past $XDG_CONFIG_HOME#34987robobun wants to merge 10 commits into
Conversation
… $HOME when XDG path has no config Two long-standing gaps between docs and behavior: 1. read_global_config() only matched install-family commands plus bunx. The global ~/.bunfig.toml's top-level fields (preload, define) and [test] section were never applied to bun <file>, bun -e, bun run, or bun test. 2. get_home_config_path() returned the $XDG_CONFIG_HOME path whenever the env var was set, without checking if the file existed. On desktops where XDG_CONFIG_HOME is set by default, $HOME/.bunfig.toml was silently ignored. The global config is now loaded for every command that loads a local bunfig (read_global_config() == LOADS_CONFIG[cmd]), and the XDG path is only used when the file actually exists there. The loaded_bunfig flag moves from load_bunfig (set on any successful parse) to load_config_path (set when the local-config load is attempted) so the run_command.rs fallbacks still fire for bun run / bun <script> after the global file has been read.
WalkthroughChangesBun now falls back from missing XDG configuration files to home-directory Configuration fallback and loading
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
|
Updated 4:21 PM PT - Jul 21st, 2026
❌ @robobun, your commit eae02b4 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 34987That installs a local version of the PR into your bun-34987 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/bunfig/arguments.rs:124-129— When~/.bunfig.tomlhas a parse error,bun repland standalone (--compile) executables now silently skip loading the local./bunfig.tomlwith no diagnostic — a regression, since pre-PR these paths ignored the global config and loaded the local one. Meanwhilebun run/bun test/bun -ecrash loudly viareport_bunfig_load_failureon the same broken file, so this is also inconsistent. Either callreport_bunfig_load_failure(ctx.log, err)in theauto_loadedarm here (matchingload_config), or fall through to the local load instead ofreturn Ok(()).Extended reasoning...
What changed
This PR flips
RunCommand.read_global_config()fromfalsetotrue(it now returnsLOADS_CONFIG[RunCommand]). That makes the pre-existing global-load block inload_config_pathnewly reachable for callers passingRunCommand:if cmd.read_global_config() { // now true for RunCommand if let Err(err) = load_global_bunfig(cmd, ctx) { if auto_loaded { return Ok(()); // <-- err dropped; local load below never runs } ...exit(1)... } } ctx.debug.loaded_bunfig = true; // <-- never reached on the swallow path load_bunfig(cmd, auto_loaded, config_path, ctx) // <-- local ./bunfig.toml — never reached
Affected entry points
Two direct callers hit this on a fresh ctx where
has_loaded_global_config == false:bun repl—repl_command.rs:50callsload_config_path(RunCommand, /*auto_loaded*/ true, "bunfig.toml", ctx).LOADS_CONFIG[ReplCommand]isfalse(ReplCommand isn't in the table), so any earlierload_config(ReplCommand, ..)skips the global block and leaveshas_loaded_global_config = false. This is the first place the global config is attempted.- Standalone executables —
run_command.rs:1150(thebun build --compileruntime path viawrite_context_no_parse) has the identical shape.
The other
RunCommandcallers (run_command.rs:931,:2397) are reached only afterload_confighas already run with aLOADS_CONFIG=truetag, which either setshas_loaded_global_config = true(makingload_global_bunfiga no-op here) or crashes viareport_bunfig_load_failure— so they don't hit this.Step-by-step proof
- User has
~/.bunfig.tomlcontainingpreload = [(unterminated array — TOML parse error). - User runs
bun replin a project with a valid./bunfig.tomlsettingpreload = ["./setup.ts"]. repl_command.rs:49seesctx.debug.loaded_bunfig == false→ callsload_config_path(RunCommand, true, "bunfig.toml", ctx).RunCommand.read_global_config()→LOADS_CONFIG[RunCommand]→true(post-PR; wasfalsepre-PR).load_global_bunfigseeshas_loaded_global_config == false, sets ittrue, callsload_bunfig(.., true, "~/.bunfig.toml", ..). The file exists soto_sourcesucceeds;Bunfig::parsereturnsErron the TOML syntax error, which propagates out ofload_global_bunfig.- Back in
load_config_path:Err(err)+auto_loaded == true→return Ok(()). The error is dropped;ctx.debug.loaded_bunfigis never set;load_bunfig(.., "bunfig.toml", ..)for the local config never runs. - Result: repl starts with neither config applied and no diagnostic printed.
Pre-PR step 4 evaluated
false, control fell straight to line 130, and./bunfig.tomlloaded — so this is a regression in the local-config load, not just a change in how the (previously-ignored) global error is handled.Why it matters / inconsistency
load_config(used bybun <file>,bun -e,bun run,bun test) handles the same failure by callingreport_bunfig_load_failure(ctx.log, err)and crashing. So the same broken~/.bunfig.tomlproduces a loud error under most commands but a silent no-config underbun repl/ compiled binaries. The standalone case is the worse one: an end-user of a compiled app may have an unrelated broken~/.bunfig.tomlfrom some other tool and will simply see the app run without its bundled-inbunfig.tomlsettings, with nothing on stderr to explain why. This is the "never swallow a failure or signal success on one" pattern from REVIEW.md §Error handling, and "fix the whole class" —load_configreports the failure, its siblingload_config_pathdirect callers do not.Fix
Either behavior is defensible, but the current silent-swallow-and-skip-local is neither:
- Match
load_config: replacereturn Ok(())withreport_bunfig_load_failure(ctx.log, err)so a broken global config fails loudly everywhere; or - Fall through: drop the
return Ok(())and let control reach the localload_bunfigcall (has_loaded_global_configis already set so it won't retry the global). This preserves the pre-PR local-load behavior while additionally trying the global first.
Given the PR's goal is uniform behavior across commands, matching
load_configis probably the right call.
…solate bun-run-bunfig.test.ts from host ~/.bunfig.toml; drop strict stderr assertions Also extend the XDG->HOME fallback to the user-level .npmrc lookup in PackageManager (fixes #23128).
|
Addressed in 8a990ae:
On the #30853 duplicate flag: that PR adds the XDG-conventional |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/config/bunfig/global-bunfig-runtime.test.ts`:
- Around line 138-151: Extend the global npmrc coverage near the existing “XDG
set but no config” test with a case where both HOME/.npmrc and XDG/.npmrc define
distinct cache paths. Run bun pm cache using baseEnv and assert it selects the
XDG cache value rather than the HOME value, covering XDG precedence in the
relevant alternate mode.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d1bedfe9-bd1f-4bdb-b932-ef66233cd296
📒 Files selected for processing (5)
src/bunfig/arguments.rssrc/install/PackageManager.rssrc/options_types/command_tag.rstest/cli/install/bun-run-bunfig.test.tstest/config/bunfig/global-bunfig-runtime.test.ts
…OME config-path helper - bunfig.rs gates for preload/serve/telemetry/smol and [run]/[install] now match RunAsNodeCommand so the global bunfig parsed during load_config(RunAsNodeCommand, ...) applies those keys. The two adjacent RunCommand/AutoCommand blocks are merged. - New home_config_path(buf, name) in bun_bunfig::arguments is shared by the .bunfig.toml and .npmrc lookups; PackageManager.rs drops its inline copy. - Added a node-shim (argv0=node) test to global-bunfig-runtime.test.ts.
…gv0 through run() helper
…two dual-parse merges - load_global_bunfig returns early when StandaloneModuleGraph::get() is Some: a compiled --compile binary must not apply the end user's ~/.bunfig.toml preload/define to the shipped app (nor crash on a malformed one). The autoloadBunfig compile flag continues to gate the cwd-local bunfig.toml only. - [test].retry/rerunEach mutual-exclusion now checks the sibling key in the current file and zeroes the counterpart on assignment, so a local file can cleanly switch a global retry to rerunEach (or vice versa). - [serve.static].bunfig_path is written alongside serve_plugins so a relative plugin path from a global [serve.static] resolves against the file that declared it.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bunfig/arguments.rs (1)
26-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRequire a usable XDG config file before preferring it.
An XDG
.bunfig.tomlor.npmrcdirectory satisfiesexists_z, suppressing a valid HOME-file fallback. The bunfig directory-read error is then silently ignored as auto-loaded. Select XDG only for a regular readable file; otherwise fall back to HOME, and add this directory case to the regression matrix.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bunfig/arguments.rs` around lines 26 - 37, Update the XDG config selection logic around env_var::XDG_CONFIG_HOME and the resolve_path::join_abs_string_buf_z call to prefer the XDG candidate only when it is a regular readable configuration file, not merely an existing directory. If the candidate is unusable, continue to the HOME-file fallback, and add a regression case covering an XDG directory at the config path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/bunfig/arguments.rs`:
- Around line 26-37: Update the XDG config selection logic around
env_var::XDG_CONFIG_HOME and the resolve_path::join_abs_string_buf_z call to
prefer the XDG candidate only when it is a regular readable configuration file,
not merely an existing directory. If the candidate is unusable, continue to the
HOME-file fallback, and add a regression case covering an XDG directory at the
config path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0c77c8a8-71fa-481e-b90e-c2116cd0ba7b
📒 Files selected for processing (5)
src/bunfig/arguments.rssrc/bunfig/bunfig.rssrc/bunfig/lib.rssrc/install/PackageManager.rstest/config/bunfig/global-bunfig-runtime.test.ts
|
On the two latest coderabbit items:
|
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/bunfig/bunfig.rs:716-722— Two more same-class one-way-merge gaps beyondretry/rerunEachandserve.static.bunfig_pathabove: (1)[run].noOrphans(bunfig.rs:~820) only callsParentDeathWatchdog::enable()whentrue— there is nodisable(), so global[run] noOrphans=truecannot be overridden by localnoOrphans=falsenow thatRunCommand/AutoCommand/RunAsNodeCommanddual-parse. (2)load_env_config()(bunfig.rs:~280, called unconditionally at :379) only ever writesdisable_default_env_files = true— theenv=true/env.file=truebranches are no-ops, so globalenv=falsecannot be re-enabled by localenv=true. For (2) anelse { … = false; }suffices; for (1)enable()does irreversible work during parse (prctl,setenv, Job Object on Windows), so store into actxfield and arm once after config loading completes.Extended reasoning...
What the bugs are
read_global_config()now returnsLOADS_CONFIG[self], soBunfig::parseruns twice (global~/.bunfig.tomlthen local./bunfig.toml, sharing onectx) forRunCommand/AutoCommand/RunAsNodeCommand/TestCommand/BuildCommand. Beyond thejsx.runtimecase fixed in f8d9ef5 and theretry/rerunEach+serve.static.bunfig_pathcases in the earlier comment, two more handlers inbunfig.rsonly ever write one direction and so cannot be overridden by the local file:(1)
[run].noOrphans— inside theis_npm_related() || RunCommand || AutoCommand || RunAsNodeCommand || TestCommandgate this hunk touches:if let Some(no_orphans) = run_expr.get(b"noOrphans") { if let Some(value) = no_orphans.as_bool() { if value { bun_io::ParentDeathWatchdog::enable(); } } // no else branch, and no ParentDeathWatchdog::disable() exists }
enable()(src/io/ParentDeathWatchdog.rs) is one-way: it doesENABLED.swap(true), callsbun_spawn_sys::pdeathsig::set_default(true)(armsPR_SET_PDEATHSIG=SIGKILLfor spawned children on Linux), doeslibc::setenv("BUN_FEATURE_FLAG_NO_ORPHANS", "1"), and on Windows creates+assigns a kill-on-close Job Object. There is nodisable()in the module.(2) top-level
env—load_env_config()is called unconditionally at bunfig.rs:379 (no command-tag gate) and its body only ever writestrue:ExprData::EBoolean(boolean) => { if !boolean.value { self.ctx.args.disable_default_env_files = true; } // no else: env=true writes nothing }
Same shape for
ENulland the nestedenv.fileboolean arm.disable_default_env_filesis consumed byrun_env_loaderinrun_command.rs/transpiler.rsto decide whether.env/.env.local/.env.developmentare auto-loaded — exactly theAutoCommand/RunCommand/TestCommandpaths this PR newly enables global→local dual-parse for.Step-by-step proof — (1) noOrphans
~/.bunfig.toml:[run]noOrphans = true./bunfig.toml:[run]noOrphans = false- Command:
bun run <script>(RunCommand)
RunCommand.read_global_config()=LOADS_CONFIG[RunCommand]=true→load_global_bunfig(RunCommand, ctx)→Bunfig::parseon~/.bunfig.toml→value == true→ParentDeathWatchdog::enable()runs immediately (atomic flipped,PR_SET_PDEATHSIGdefault set, env var exported).Run::boot→load_config_path(RunCommand, …)→Bunfig::parseon./bunfig.tomlwith the samectx→value == false→ theif valuebody is skipped. Nothing un-flips the atomic or clears the env var.- Every subprocess spawned via
bun rungetsPR_SET_PDEATHSIG=SIGKILLon Linux despite the local override.
Pre-PR, step 1 did not happen for
RunCommand/AutoCommand/RunAsNodeCommand(they were absent from the oldmatches!), so the observable pre-PR result for this scenario was "watchdog disabled" (global ignored entirely). Post-PR it is "watchdog enabled" — a narrow regression against the documented shallow-merge contract. (is_npm_related()commands did dual-parse pre-PR, so the gap was technically reachable forbun installlifecycle scripts, but[run].noOrphansis primarily a run-family setting.)Step-by-step proof — (2) env
~/.bunfig.toml:env = false./bunfig.toml:env = true- Command:
bun ./main.ts(AutoCommand)
AutoCommand.read_global_config()=true→ global parse →load_env_config:!boolean.value→ctx.args.disable_default_env_files = true.- Local
bunfig.tomlauto-loaded (positional matchesDEFAULT_LOADERS) →load_env_config:boolean.value == true→ theif !boolean.valuebody is skipped. Field staystrue. run_env_loader(disable_default_env_files=true)→.envfiles are not loaded despite the local override.
Not a strict regression (pre-PR, global
env=falsewas never applied to runtime commands at all, so.envloaded either way for this scenario), but it violates the local-overrides-global contract this PR is enabling — same class as thejsx.runtimegap already fixed in f8d9ef5.Why existing code doesn't prevent it
Both handlers were written when
Bunfig::parseran at most once for the commands that consume these flags, so "false == default == write nothing" was correct. The dual-parse this PR enables makes the missingfalsebranch observable. Neither is a duplicate of the earlier comment: that one enumerates[test].retry/rerunEachandserve.static.bunfig_path; these are two distinct keys with distinct fixes.Suggested fix
env: add theelse { self.ctx.args.disable_default_env_files = false; }branch to both the top-levelEBooleanarm and the nestedenv.fileEBooleanarm, so last-write-wins matches the other keys.noOrphans: becauseenable()performs irreversible side effects during parse (setenv, prctl default, Windows Job Object), anelse-branch alone can't undo it. Store the value into actxfield on both branches (last-write-wins) and callParentDeathWatchdog::enable()once after config loading completes, mirroring how other deferred side effects are handled.
Both are narrow triggers (user must set the key one way globally AND explicitly the other way locally), hence nit — same treatment as the earlier same-class comment.
|
On the two remaining dual-parse one-way-merge items (
Both are good candidates for a dedicated "make bunfig.rs dual-parse-safe" follow-up that walks every handler once. |
|
CI status on eae02b4 (build 77131): the new
The |
|
Triage note: #36486 is the PR being kept for #23128 (the |
|
Closing as a duplicate. The |
Related: #23128 (the
$XDG_CONFIG_HOME->$HOMEfallback itself is being landed via #36486; this PR is kept for loading the global bunfig in runtime commands, and its copy of the fallback can be dropped on rebase).Repro
And with
XDG_CONFIG_HOMEset to an empty dir (standard on most desktops),[install] saveTextLockfile = falsein$HOME/.bunfig.tomlis ignored bybun install.Cause
Two gaps between the documented behavior and the loader in
src/bunfig/arguments.rs:Tag::read_global_config()only matched install-family commands plusbunx.AutoCommand,RunCommand,RunAsNodeCommand,TestCommandandBuildCommandnever loaded the global file at all, so itspreload,define, and[test]fields had no effect underbun <file>/bun -e/bun run/bun test.get_home_config_path()returned the$XDG_CONFIG_HOME/.bunfig.tomlpath whenever the env var was set, without checking that the file exists. On a desktop whereXDG_CONFIG_HOMEis set (most of them),$HOME/.bunfig.tomlwas silently dead even for install commands.Fix
read_global_config()now matchesLOADS_CONFIG[self]: every command that loads a localbunfig.tomlloads the global one first, so the documented shallow merge applies uniformly.get_home_config_path()checksbun_sys::exists_zon the XDG candidate and falls through to$HOME/.bunfig.tomlwhen there is no file at the XDG path. Both env vars are read withget_not_empty()so an empty string is treated as unset.loaded_bunfigflag moves fromload_bunfig(set on any successful parse) toload_config_path(set when the local-config load is attempted). Without this, loading the global file would set the flag and short-circuit therun_command.rs/repl_command.rslocal-load fallbacks used bybun run <script>.load_config()'s global block now callsload_global_bunfig()directly instead of routing throughload_config_pathwith the home path; same effect, one less redundant path computation.Tests
test/config/bunfig/global-bunfig-runtime.test.tscoversbun <file>,bun -e,bun run,bun test, the local-overrides-global merge forbun run, the XDG→HOME fallback (via both a runtime command andbun pm cache), and XDG-wins-when-present.test/cli/install/bun-run-bunfig.test.tspreviously had a characterization test asserting the home bunfig is not loaded forbun run; it now asserts it is, matching the docs.The XDG->HOME fallback here also covers the user-level
.npmrclookup inPackageManager; both fallback hunks duplicate #36486 and are not what this PR is kept open for.Related: #30853 extends the XDG lookup to the conventional
$XDG_CONFIG_HOME/bun/bunfig.tomlpath and will rebase cleanly on top of this (the fallback logic here is a strict subset).no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-run-bunfig.test.ts