feat(system): declarative system packages (apt, dnf, pacman, and brew without brew)#10326
Conversation
|
Warning Review limit reached
More reviews will be available in 2 minutes and 45 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthroughThis PR introduces a comprehensive system-package management feature for mise, allowing declarative installation of OS-level packages via ChangesSystem package management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
|
Greptile SummaryAdds an experimental
Confidence Score: 5/5Safe to merge behind the experimental gate; all issues from the first review pass have been resolved and the binary relocation paths are backed by unit tests. The privilege-elevation path is tightly scoped and well-audited. Binary relocation correctly handles both in-place and grow cases for ELF and Mach-O. The linked_version() fix ensures a partially-installed (unlinked) keg is not mistaken for a successful install. Config merging uses the same .rev() ordering the rest of the codebase relies on. No new logic bugs were found beyond two minor edge cases that don't affect real-world Homebrew bottles. src/system/packages/brew/elf.rs and src/system/packages/brew/pour.rs; the ELF patcher is intricate and the ledger error-propagation could be refined. Important Files Changed
Reviews (12): Last reviewed commit: "fix(system): count skipped version pins ..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (1)
xtasks/fig/src/mise.ts (1)
3201-3203: ⚡ Quick winAdd manager suggestions for
--managerto improve completion accuracy.
manageris currently free-form. Adding known manager suggestions (apt,dnf,pacman,brew) will reduce invalid completions and better match the command contract.Suggested diff
args: { name: "manager", + suggestions: ["apt", "dnf", "pacman", "brew"], },🤖 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 `@xtasks/fig/src/mise.ts` around lines 3201 - 3203, The "manager" arg currently defined as args: { name: "manager" } should include a fixed set of suggestions to improve completion accuracy; update the arg definition for name "manager" (the args object for the manager flag) to add a suggestions array or suggestion provider with the values ["apt", "dnf", "pacman", "brew"] so completions present these known package managers instead of leaving it free-form.
🤖 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 `@e2e/cli/test_system_status`:
- Around line 25-27: The test's "empty section" case writes the wrong TOML
section (`[tools]`) so it doesn't validate an empty `[system.packages]` config;
update the test content that writes to "mise.toml" to emit `[system.packages]`
(and any nested/expected structure used by the validation logic) instead of
`[tools]` so the case actually targets the `system.packages` table referenced by
the validation code.
In `@src/cli/doctor/mod.rs`:
- Around line 363-365: doctor_json() is skipping system-package diagnostics
because analyze_system_packages(config).await is only invoked via
analyze_config(); update the JSON path to call the same helper so JSON output
matches text mode—either have doctor_json() call
self.analyze_system_packages(config).await (and merge its results into the JSON
payload) or refactor so doctor_json() reuses analyze_config()'s result; ensure
you reference analyze_system_packages(), analyze_config(), and doctor_json() and
propagate/serialize any errors or findings into the JSON response just like the
text path.
In `@src/system/packages/apt.rs`:
- Around line 44-47: dpkg_name currently strips architecture qualifiers (e.g.,
"gcc:arm64"), causing queries and status checks to conflate architectures;
change the logic so the architecture qualifier is preserved for queries and
matching: update or replace dpkg_name to return both base name and optional arch
(e.g., parse into (pkg, arch) or provide dpkg_arch), use the original name:arch
when constructing dpkg/apt queries, and when comparing installed package status
(the code paths that currently call dpkg_name for matching), compare both
package name and architecture (or match against a returned Package:Architecture
string) so "gcc:arm64" only matches an installed arm64 package and not amd64.
Ensure all callers that previously relied on dpkg_name are updated to use the
new parsed pair or arch-aware comparison.
In `@src/system/packages/brew/macho.rs`:
- Around line 84-89: Before calling u32_at(slice, off) in the load-command
parsing loops, validate that off + 8 is within the declared load-command table
bound so we don't read past the table and panic; specifically, in the loop that
uses HEADER_SIZE_64 and ncmds (and the second loop around the 113-116 region)
check that off + 8 <= HEADER_SIZE_64 + sizeofcmds (or equivalently ensure off +
8 <= slice.len() && off < slice.len()) before reading cmd and cmdsize, and bail!
with the existing error path if that check fails; apply the same guard to both
occurrences where u32_at(slice, off) is called.
In `@src/system/packages/brew/pour.rs`:
- Around line 28-45: installed_versions currently sorts kegs by preserved
directory mtimes (in installed_versions) which is incorrect; instead read the
active opt symlink (prefix::opt().join(name)) via std::fs::read_link to
determine the currently active keg and use that as the primary source of truth:
collect all keg directory names as you already do in installed_versions, detect
the opt symlink target basename and, if it matches one of the collected
versions, move that version to the front of the returned Vec; if the opt symlink
is missing or invalid, fall back to the existing deterministic ordering (e.g.,
name sort or the current mtime-based fallback) so behavior remains stable.
- Around line 100-105: The code moves the temp keg into place with
crate::file::rename(&tmp, &keg) before calling link_keg(name, &pkg_version,
rf.formula.keg_only), which can leave the formula half-installed if linking
fails; either preflight link conflicts before the rename (call the link
validation logic used by link_keg or a new preflight_link_check(name,
&pkg_version, rf.formula.keg_only) and abort before moving the keg) or perform
the rename and then roll back on error (if link_keg returns Err, remove the
moved keg via crate::file::remove_all(&keg)? and restore the tmp via
crate::file::rename(&backup_tmp, &keg)? or return the original tmp to its
previous name), and ensure keg_installed() only returns true after both rename
and successful linking and ledger recording complete.
In `@src/system/packages/brew/prefix.rs`:
- Around line 91-100: The code uses crate::env::var("USER") with a "root"
fallback when building chown_args (used in the needs_create || needs_chown
branch), causing incorrect ownership when USER is unset; change this to resolve
the username from the effective UID (e.g., call
libc::geteuid()/nix::unistd::getuid() and lookup the passwd entry via getpwuid
or the users crate) and use that resolved username in the
format!("{user}:admin") for chown_args; if the UID-to-username lookup fails,
return an explicit error instead of defaulting to "root" so we don't
accidentally chown to root:admin and then fail writability checks.
In `@src/system/packages/brew/resolve.rs`:
- Around line 22-45: The dependency resolver currently always uses host_tag when
calling Formula::dependencies_for, causing it to miss dependencies for the
actual bottle chosen by tag::select; update resolve_closure() to call
tag::select (or the equivalent selection routine used elsewhere) for each
formula to obtain the actual selected bottle tag/variation (e.g., bottle_tag =
tag::select(&formula.variations) or similar) and pass that bottle_tag into
formula.dependencies_for(&bottle_tag) instead of &host_tag; apply the same fix
to the other occurrence referenced in the comment (the block around the later
76-90 region) so both places compute dependencies from the selected bottle tag
rather than always from host_tag.
In `@src/system/packages/brew/state.rs`:
- Around line 29-42: load() treats parse failures as empty and save() overwrites
brew.json in-place, which can corrupt state if interrupted; change save() to
write JSON to a temporary file adjacent to ledger_path() (e.g., ledger_path() ->
tmp path like ledger_path().with_extension("tmp") or similar), fsync the temp
file, then atomically rename the temp to the final path (using rename), and
ensure parent dir exists via crate::file::create_dir_all before creating the
temp; keep serde_json::to_string_pretty(self) as the content and propagate
errors as Result, and leave load() behavior unchanged aside from reading the
final file produced by the atomic rename.
In `@src/system/packages/dnf.rs`:
- Around line 80-87: The current rpm -q error branch bails whenever stdout is
empty and stderr is non-empty, which treats all-missing queries as fatal; change
the condition in the rpm-query handling (the block that checks output.status,
output.stdout, output.stderr and calls bail!) to only bail on real errors by
inspecting output.stderr contents: if stderr contains lines other than the
expected "package X is not installed" messages (or otherwise does not match the
"is not installed" pattern), then bail with the stderr; otherwise skip bailing
and allow the existing parser to interpret missing packages as Missing. Ensure
you reference the same output.status/output.stdout/output.stderr variables and
preserve the existing bail! call for genuine errors.
In `@src/system/packages/pacman.rs`:
- Around line 96-105: The current code treats any non-success from the pacman -Q
command as "missing" by always calling parse_pacman_query(&stdout, pkgs); change
the logic to detect real command failures: if output.status is not success and
output.stdout is empty, return an Err containing the command stderr (or a
wrapped error) instead of calling parse_pacman_query; keep the existing behavior
only when stdout contains data to parse. Update the branch around the existing
debug/if block (referencing output, stdout, pkgs, and parse_pacman_query) so
real pacman/database errors surface as errors rather than being mapped to
Missing.
---
Nitpick comments:
In `@xtasks/fig/src/mise.ts`:
- Around line 3201-3203: The "manager" arg currently defined as args: { name:
"manager" } should include a fixed set of suggestions to improve completion
accuracy; update the arg definition for name "manager" (the args object for the
manager flag) to add a suggestions array or suggestion provider with the values
["apt", "dnf", "pacman", "brew"] so completions present these known package
managers instead of leaving it free-form.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 98d73f92-1817-4126-b1f3-252670da4569
📒 Files selected for processing (43)
docs/.vitepress/cli_commands.tsdocs/.vitepress/config.tsdocs/cli/index.mddocs/cli/system.mddocs/cli/system/install.mddocs/cli/system/status.mddocs/system-packages/apt.mddocs/system-packages/brew.mddocs/system-packages/dnf.mddocs/system-packages/index.mddocs/system-packages/pacman.mde2e/cli/test_system_install_apte2e/cli/test_system_statusman/man1/mise.1mise.usage.kdlschema/mise.jsonsettings.tomlsrc/cli/doctor/mod.rssrc/cli/install.rssrc/cli/mod.rssrc/cli/system/install.rssrc/cli/system/mod.rssrc/cli/system/status.rssrc/config/config_file/mise_toml.rssrc/config/config_file/mod.rssrc/main.rssrc/system/mod.rssrc/system/packages/apt.rssrc/system/packages/brew/api.rssrc/system/packages/brew/fetch.rssrc/system/packages/brew/macho.rssrc/system/packages/brew/mod.rssrc/system/packages/brew/pour.rssrc/system/packages/brew/prefix.rssrc/system/packages/brew/relocate.rssrc/system/packages/brew/resolve.rssrc/system/packages/brew/state.rssrc/system/packages/brew/tag.rssrc/system/packages/dnf.rssrc/system/packages/mod.rssrc/system/packages/pacman.rssrc/system/sudo.rsxtasks/fig/src/mise.ts
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.3 x -- echo |
20.5 ± 1.0 | 18.2 | 26.3 | 1.00 |
mise x -- echo |
21.5 ± 1.5 | 19.2 | 38.7 | 1.05 ± 0.09 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.3 env |
20.2 ± 1.1 | 18.1 | 26.5 | 1.00 |
mise env |
20.8 ± 1.2 | 18.7 | 27.8 | 1.03 ± 0.08 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.3 hook-env |
20.9 ± 1.0 | 19.0 | 27.7 | 1.00 |
mise hook-env |
21.5 ± 1.1 | 19.4 | 27.6 | 1.03 ± 0.07 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.3 ls |
17.2 ± 1.1 | 15.4 | 23.3 | 1.00 |
mise ls |
17.7 ± 0.9 | 16.1 | 21.9 | 1.03 ± 0.08 |
xtasks/test/perf
| Command | mise-2026.6.3 | mise | Variance |
|---|---|---|---|
| install (cached) | 140ms | 141ms | +0% |
| ls (cached) | 62ms | 61ms | +1% |
| bin-paths (cached) | 67ms | 68ms | -1% |
| task-ls (cached) | 130ms | 130ms | +0% |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/system/packages/brew/mod.rs (1)
53-61:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not turn
pkg_version()failures into “already poured”.This filter maps any
pkg_version()error tofalse, so a malformed formula version drops out ofto_poursilently. If that happens for every requested formula, Line 62 returns success withbrew: all formulae already pouredeven though the install never ran.Suggested fix
- let to_pour: Vec<_> = closure - .iter() - .filter(|rf| { - rf.formula - .pkg_version() - .map(|v| !pour::keg_installed(&rf.formula.name, &v)) - .unwrap_or(false) - }) - .collect(); + let to_pour: Vec<_> = closure + .iter() + .filter_map(|rf| match rf.formula.pkg_version() { + Ok(v) if !pour::keg_installed(&rf.formula.name, &v) => Some(Ok(rf)), + Ok(_) => None, + Err(err) => Some(Err(err)), + }) + .collect::<Result<Vec<_>>>()?;🤖 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/system/packages/brew/mod.rs` around lines 53 - 61, The current filter on closure turns any pkg_version() Err into false, silently excluding malformed formulas from to_pour; change the logic so pkg_version() errors are not treated as "already poured" but instead cause an early error (propagate a Result Err) or otherwise include the problematic formula in to_pour. Concretely, update the closure used to build to_pour (the filter around rf.formula.pkg_version() and pour::keg_installed) to match on pkg_version() and on Err return/bubble a descriptive error (including rf.formula.name) from the surrounding function rather than mapping to false, so malformed versions don't silently pass as installed.
♻️ Duplicate comments (1)
src/system/packages/brew/prefix.rs (1)
96-100:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not fall back to
$USERfor a recursivechown.If
User::from_uid(geteuid())fails,$USERcan still be stale (sudo -u, minimal container env, injected shell state). That makes the recursivechown -Rhand the machine-global prefix to the wrong account. Fail here instead of trusting the environment.Suggested fix
let Some(user) = nix::unistd::User::from_uid(nix::unistd::geteuid()) .ok() .flatten() .map(|u| u.name) - .or_else(|| crate::env::var("USER").ok()) else { // never chown to a guessed owner — that can lock the user out bail!(🤖 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/system/packages/brew/prefix.rs` around lines 96 - 100, The code currently falls back to crate::env::var("USER") when nix::unistd::User::from_uid(nix::unistd::geteuid()) fails; remove that fallback and instead surface a hard failure so we don't trust a possibly stale $USER for recursive chown. Replace the or_else(|| crate::env::var("USER").ok()) path so that when User::from_uid(geteuid()) returns None (or Err) the calling function returns an error (or uses expect with a clear message) indicating failure to resolve the current UID to a user; reference the User::from_uid and nix::unistd::geteuid call sites and ensure any downstream use (the chown -R owner resolution) only proceeds when a valid username was obtained.
🤖 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 `@src/system/packages/brew/prefix.rs`:
- Around line 48-57: The bootstrap routine currently creates SUBDIRS under
prefix(), but repository() on Linux is prefix().join("Homebrew") so relocated
paths (see relocate::standard_replacements()) point inside prefix()/Homebrew
which never gets created; update bootstrap() (and any helper that builds SUBDIRS
in the 67-137 region) to create the repository tree returned by repository()
instead of prefix() on Linux — e.g., call repository() to derive the base and
ensure it creates repository()/Library and all other expected SUBDIRS so
relocated paths exist after bootstrap.
---
Outside diff comments:
In `@src/system/packages/brew/mod.rs`:
- Around line 53-61: The current filter on closure turns any pkg_version() Err
into false, silently excluding malformed formulas from to_pour; change the logic
so pkg_version() errors are not treated as "already poured" but instead cause an
early error (propagate a Result Err) or otherwise include the problematic
formula in to_pour. Concretely, update the closure used to build to_pour (the
filter around rf.formula.pkg_version() and pour::keg_installed) to match on
pkg_version() and on Err return/bubble a descriptive error (including
rf.formula.name) from the surrounding function rather than mapping to false, so
malformed versions don't silently pass as installed.
---
Duplicate comments:
In `@src/system/packages/brew/prefix.rs`:
- Around line 96-100: The code currently falls back to crate::env::var("USER")
when nix::unistd::User::from_uid(nix::unistd::geteuid()) fails; remove that
fallback and instead surface a hard failure so we don't trust a possibly stale
$USER for recursive chown. Replace the or_else(|| crate::env::var("USER").ok())
path so that when User::from_uid(geteuid()) returns None (or Err) the calling
function returns an error (or uses expect with a clear message) indicating
failure to resolve the current UID to a user; reference the User::from_uid and
nix::unistd::geteuid call sites and ensure any downstream use (the chown -R
owner resolution) only proceeds when a valid username was obtained.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ba750b98-399d-4d7b-9bed-01b80c18ec2f
📒 Files selected for processing (26)
completions/_misedocs/cli/system/install.mddocs/system-packages/brew.mddocs/system-packages/index.mde2e/cli/test_system_install_brew_linuxe2e/cli/test_system_statuse2e/tasks/test_task_completione2e/tasks/test_task_completion_global_cdmise.usage.kdlsrc/cli/doctor/mod.rssrc/cli/system/install.rssrc/system/mod.rssrc/system/packages/apt.rssrc/system/packages/brew/macho.rssrc/system/packages/brew/mod.rssrc/system/packages/brew/pour.rssrc/system/packages/brew/prefix.rssrc/system/packages/brew/relocate.rssrc/system/packages/brew/resolve.rssrc/system/packages/brew/state.rssrc/system/packages/brew/tag.rssrc/system/packages/dnf.rssrc/system/packages/mod.rssrc/system/packages/pacman.rssrc/system/sudo.rsxtasks/fig/src/mise.ts
✅ Files skipped from review due to trivial changes (4)
- e2e/cli/test_system_install_brew_linux
- docs/cli/system/install.md
- docs/system-packages/brew.md
- docs/system-packages/index.md
🚧 Files skipped from review as they are similar to previous changes (14)
- src/cli/doctor/mod.rs
- src/system/packages/brew/state.rs
- src/system/packages/brew/tag.rs
- xtasks/fig/src/mise.ts
- src/system/mod.rs
- src/system/packages/pacman.rs
- src/system/packages/mod.rs
- e2e/cli/test_system_status
- src/system/packages/dnf.rs
- src/system/packages/brew/macho.rs
- src/system/packages/brew/pour.rs
- src/system/sudo.rs
- src/system/packages/apt.rs
- mise.usage.kdl
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/system/packages/brew/prefix.rs (1)
162-165:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSudo mkdir path doesn't create the Linux repository tree.
The sudo elevation path builds its directory list from
SUBDIRSdirectly (line 163), but the Linux repository tree (<prefix>/Homebrew/Library) is only added inbootstrap_dirs(). When elevation is needed, the repository tree won't be created. This is inconsistent with the non-elevated path at line 138 which usesdirsfrombootstrap_dirs().Suggested fix
- let mut dirs: Vec<String> = vec![prefix.to_string_lossy().to_string()]; - dirs.extend(SUBDIRS.iter().map(|d| prefix.join(d).display().to_string())); + let mut mkdir_dirs: Vec<String> = vec![prefix.to_string_lossy().to_string()]; + mkdir_dirs.extend(dirs.iter().map(|d| d.display().to_string()));🤖 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/system/packages/brew/prefix.rs` around lines 162 - 165, The sudo-elevation branch builds dirs from SUBDIRS directly so the Linux repo tree (added by bootstrap_dirs()) is omitted; change the code that constructs dirs for the elevated path to call or reuse bootstrap_dirs(prefix) (the same source used by the non-elevated path) so dirs includes the Homebrew/Library entries, then build mkdir_args and chown_args from that expanded dirs vector (variables: dirs, SUBDIRS, bootstrap_dirs(), mkdir_args, chown_args).
🤖 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 `@src/system/packages/brew/prefix.rs`:
- Around line 77-84: The brewed_glibc detection hardcodes the x86_64 loader
filename, so update the logic that builds `brewed_glibc` (the
`crate::file::ls(&cellar().join("glibc"))...filter_map` block) to look for the
dynamic loader generically rather than only "lib/ld-linux-x86_64.so.2": e.g.,
inspect files under each keg's lib directory and accept any entry whose filename
starts with "ld-linux-" and contains ".so" (or check both known names
"ld-linux-x86_64.so.2" and "ld-linux-aarch64.so.1" based on target arch), then
return the first match as `candidate`; keep using `candidate.exists()` semantics
and `next_back()` as before.
---
Outside diff comments:
In `@src/system/packages/brew/prefix.rs`:
- Around line 162-165: The sudo-elevation branch builds dirs from SUBDIRS
directly so the Linux repo tree (added by bootstrap_dirs()) is omitted; change
the code that constructs dirs for the elevated path to call or reuse
bootstrap_dirs(prefix) (the same source used by the non-elevated path) so dirs
includes the Homebrew/Library entries, then build mkdir_args and chown_args from
that expanded dirs vector (variables: dirs, SUBDIRS, bootstrap_dirs(),
mkdir_args, chown_args).
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c4092bc0-7beb-4211-9f45-2534c7b2100c
📒 Files selected for processing (13)
docs/system-packages/brew.mde2e/cli/test_system_install_apte2e/run_testsrc/cli/install.rssrc/cli/system/install.rssrc/system/packages/apt.rssrc/system/packages/brew/elf.rssrc/system/packages/brew/mod.rssrc/system/packages/brew/pour.rssrc/system/packages/brew/prefix.rssrc/system/packages/brew/relocate.rssrc/system/packages/dnf.rssrc/system/sudo.rs
✅ Files skipped from review due to trivial changes (1)
- docs/system-packages/brew.md
🚧 Files skipped from review as they are similar to previous changes (5)
- src/system/sudo.rs
- src/system/packages/brew/mod.rs
- e2e/cli/test_system_install_apt
- src/cli/system/install.rs
- src/system/packages/dnf.rs
Adds a new [system] config namespace for machine-global bootstrapping,
starting with declarative system packages:
[system.packages]
apt = ["libssl-dev", "curl=8.5.0-2"]
- new src/system/ subsystem (separate from Backend: unversioned,
machine-global, no shims/lockfile)
- apt manager: dpkg-query state checks, apt-get install, automatic
apt-get update for fresh containers
- single auditable sudo chokepoint: skips sudo as root, interactive
prompt with TTY, sudo -n probe otherwise, full-argv logging,
disable with system_packages.sudo=false
- mise system install/status commands (experimental-gated); packages
are only ever installed explicitly, never during mise install
- one-time hint in mise install + mise doctor section
- docs, JSON schema, e2e tests
brew (Homebrew bottles without brew installed) comes separately.
Co-Authored-By: Claude Fable 5 <[email protected]>
…docs brew (arm64 macOS): installs homebrew/core formulae into /opt/homebrew without brew installed — formulae.brew.sh metadata, dep closure resolution, ghcr.io bottle downloads with sha256 verification, full pour-time relocation (text placeholders, in-place binary C-strings, and proper Mach-O load-command rewriting that grows cmdsize into header padding like ruby-macho), mandatory arm64 ad-hoc re-codesigning, brew-compatible INSTALL_RECEIPT.json so a later-installed real brew adopts the kegs, opt/prefix linking with keg-only and conflict handling, and delegation to real brew when one owns the prefix. dnf (Fedora/RHEL) and pacman (Arch) managers with rpm -q / pacman -Q state checks and sudo-elevated installs. system_packages.managers setting restricts which managers run when several could apply. Docs split into an overview plus a page per manager. Co-Authored-By: Claude Fable 5 <[email protected]>
The render task picked up a stale usage-cli 3.3.0 from PATH instead of the mise-managed 3.5.0, regressing the zsh completion wrapper to an old template. The system subcommands are completed dynamically via `mise usage`, so no regeneration is needed. Co-Authored-By: Claude Fable 5 <[email protected]>
mise system install/status already require experimental; the passive paths (missing-packages hint in mise install, doctor section) now skip silently without it instead of running package state checks. Co-Authored-By: Claude Fable 5 <[email protected]>
brew manager now supports Linux (x86_64 + arm64): bottles pour into
/home/linuxbrew/.linuxbrew with per-OS placeholder values
(@@HOMEBREW_REPOSITORY@@ -> <prefix>/Homebrew on Linux), per-arch bottle
tags (x86_64_linux/arm64_linux), no codesigning on Linux, and per-OS
chown in prefix bootstrap. mise never dispatches to a real brew anymore
— it always pours itself; receipts remain brew-compatible so the two
interoperate through the Cellar.
Compile brew module only on unix (fixes Windows build).
Review fixes (CodeRabbit/Greptile/Bugbot):
- sudo: env vars now pass through `sudo env K=V cmd` — sudo's
env_reset was dropping DEBIAN_FRONTEND before it reached apt-get
- dnf/pacman: only real errors fail status checks; all-missing parses
as Missing
- apt: arch-qualified requests (gcc:arm64) match by name:arch instead
of conflating architectures
- resolve: alias->canonical map avoids re-fetching formulae; deps come
from the selected bottle tag's variations, not the host tag
- pour: active keg from the opt symlink (not archive mtimes); link
failure rolls the keg back instead of leaving it half-installed
- prefix: chown owner derived from euid, error instead of guessing root
- macho: bounds-check load-command reads against sizeofcmds
- state: atomic ledger writes (tmp + rename)
- doctor -J now includes system_packages like text mode
- --manager gets clap value choices (completions list managers)
- e2e: linux brew pour test; empty [system.packages] case
Also fix test_task_completion{,_global_cd} for usage-cli 3.5.0's new
3-column complete-word output (broken on main too).
Co-Authored-By: Claude Fable 5 <[email protected]>
…iew fixes Linux bottles ship with @@HOMEBREW_PREFIX@@ placeholders in their ELF interpreter and rpath, which grow when restored (19 -> 26 bytes) and so cannot be patched in place. Add src/system/packages/brew/elf.rs, a port of the patchelf strategy brew uses via its PatchELF gem: strings that no longer fit move into a new PT_LOAD segment appended to the binary along with a relocated program header table. - mirror brew's Linux pour rules: ELF interp/rpath + text files only; honor :any_skip_relocation only for bottles built by Homebrew >= 5.1.15 (extend/os/linux/bottle_specification.rb); skip glibc; rpath component filtering, gcc/current rewrite, and <prefix>/lib appending - create <prefix>/lib/ld.so pointing at the host loader (or brewed glibc) - e2e harness: mount a writable /home in the read-only Docker sandbox so the linuxbrew test can create its prefix; tests that install real system packages opt into a writable rootfs via an e2e:writable-rootfs marker (apt needs /var + /usr) - restore completions/_mise (was rendered with a stale usage-cli 3.5.0 renders match main exactly) - review fixes: propagate pkg_version errors, drop $USER fallback when deriving the prefix owner, bootstrap the Linux repository tree, include DEBIAN_FRONTEND in apt dry-run output, drop the '?' alias warning
…ootstrap dirs via sudo - setup_linux_runtime now prefers a brewed glibc loader (correct ld-linux-x86-64/aarch64 names) and repoints the symlink even when ld.so already exists; runs again after pours so a glibc poured in the same run takes effect - dnf: an installed base package no longer satisfies a longer hyphenated request (glib2 vs glib2-devel) — NVR specs must have a digit after the name - prefix bootstrap: the elevated mkdir path now creates the Linux repository tree too, not just SUBDIRS
…ger error - mise doctor (text and -J) now emits one aggregated warning for missing system packages instead of an identical warning per manager - mise system install --manager X now says when X was excluded by the system_packages.managers setting rather than claiming no packages are configured for it
- macho.rs: rewrite placeholder loop as while-let (clippy::while_let_loop) - elf.rs: simplify interp fit check (clippy::int_plus_one) - resolve.rs: allow too_many_arguments on the topo-sort visitor - tag.rs: elide needless lifetimes in tag::select - pour.rs: the link-conflict error no longer claims the keg is usable (the caller rolls it back), and a failed rollback now warns with the keg path instead of being silently swallowed Co-Authored-By: Claude Fable 5 <[email protected]>
When mise itself runs under sudo, the brew prefix bootstrap used the effective uid (root) for both the writability check and ownership: an existing user-owned prefix would be destructively chown -R'd to root, and a fresh prefix would be created root-owned, breaking later non-root installs. Root now treats any existing prefix as writable, a fresh prefix is chowned to SUDO_USER (like brew's install.sh), and pouring under sudo warns that keg files will be root-owned. Co-Authored-By: Claude Fable 5 <[email protected]>
…hanges The 24h marker now stores a fingerprint of the configured package set; editing the config re-checks immediately instead of waiting out the throttle. Co-Authored-By: Claude Fable 5 <[email protected]>
…bottles Co-Authored-By: Claude Fable 5 <[email protected]>
A failed upgrade link previously removed the old version's links without putting them back; now every replaced symlink is restored on rollback. Co-Authored-By: Claude Fable 5 <[email protected]>
An NVR spec like bash-5.2.26-3.fc40 previously matched any installed bash; the version(-release) suffix is now compared and a differing installed version reports as VersionMismatch, matching apt's pin handling. Co-Authored-By: Claude Fable 5 <[email protected]>
fdcb8de to
90d3a86
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@docs/system-packages/index.md`:
- Around line 12-16: Update the sentence in docs that reads "System packages ...
are managed by the OS package manager" (the paragraph starting with "System
packages are intentionally separate from [`[tools]`](/configuration.html):") to
clarify the exception for the brew backend: state that system packages are
generally managed by the OS package manager, but the brew backend in this PR can
bypass the Homebrew CLI and pour bottles directly, so users should not assume
the normal Homebrew CLI is required; reword the paragraph to avoid implying
every backend uses the Homebrew CLI and add a short note mentioning the brew
backend exception.
- Around line 32-35: Update the "OS-filtered" paragraph to stop claiming "brew
entries on Linux are ignored" and to accurately reflect runtime behavior: note
that package managers are filtered by OS where unsupported (but Homebrew is
supported on Linux in this PR), and clarify that unsupported managers are
skipped for operations like install/apply but are still reported by the status
and doctor commands; specifically edit the paragraph referencing "OS-filtered",
mention "brew" support on Linux, and call out "status" and "doctor" as the
commands that surface unavailable managers.
In `@src/system/packages/apt.rs`:
- Around line 69-84: parse_dpkg_query currently collapses multiple installed
rows for a bare package name into a single (status, version) in the installed
map, causing order-dependent behavior; change parse_dpkg_query so that for a
given package bare name it preserves all installed versions (e.g., store
Vec<String> of versions or at minimum keep a set of installed versions) instead
of overwriting only when existing status != "installed", and then when building
the result for each request (requests -> state) prefer an exact
requested-version match among those preserved installed versions before falling
back to a generic stored version or reporting VersionMismatch; update logic that
reads the installed map (the code that does installed.get(&req.name) and matches
Some(("installed", version))) to check for a matching version in the preserved
list and return PackageState::Installed with that version when found.
- Around line 147-154: AptManager::install currently appends PackageRequest.raw
directly to apt-get args, allowing malicious or config-like entries (e.g.
-oDebug::...) to be parsed as options; fix by inserting the end-of-options
marker "--" into the args vector before extending with any pkg.raw values in
install so all remaining entries are treated strictly as package operands. Also
update parse_dpkg_query's bare-name multiarch aggregation logic so it doesn't
keep only the first "installed" version: collect all installed versions per base
name and choose deterministically (e.g., prefer exact version-pin matches when
present, otherwise use a stable selection like highest-version-or-arch order) so
a bare-name + version pin is not order-dependent across architectures.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4f7c8258-e5b9-467c-b59a-fb64539acf04
📒 Files selected for processing (47)
docs/.vitepress/cli_commands.tsdocs/.vitepress/config.tsdocs/cli/index.mddocs/cli/system.mddocs/cli/system/install.mddocs/cli/system/status.mddocs/system-packages/apt.mddocs/system-packages/brew.mddocs/system-packages/dnf.mddocs/system-packages/index.mddocs/system-packages/pacman.mde2e/cli/test_system_install_apte2e/cli/test_system_install_brew_linuxe2e/cli/test_system_statuse2e/run_testman/man1/mise.1mise.usage.kdlschema/mise.jsonsettings.tomlsrc/cli/doctor/mod.rssrc/cli/install.rssrc/cli/mod.rssrc/cli/system/install.rssrc/cli/system/mod.rssrc/cli/system/status.rssrc/config/config_file/mise_toml.rssrc/config/config_file/mod.rssrc/hint.rssrc/main.rssrc/system/mod.rssrc/system/packages/apt.rssrc/system/packages/brew/api.rssrc/system/packages/brew/elf.rssrc/system/packages/brew/fetch.rssrc/system/packages/brew/macho.rssrc/system/packages/brew/mod.rssrc/system/packages/brew/pour.rssrc/system/packages/brew/prefix.rssrc/system/packages/brew/relocate.rssrc/system/packages/brew/resolve.rssrc/system/packages/brew/state.rssrc/system/packages/brew/tag.rssrc/system/packages/dnf.rssrc/system/packages/mod.rssrc/system/packages/pacman.rssrc/system/sudo.rsxtasks/fig/src/mise.ts
✅ Files skipped from review due to trivial changes (6)
- docs/cli/system.md
- docs/.vitepress/config.ts
- docs/cli/index.md
- docs/system-packages/pacman.md
- docs/cli/system/install.md
- docs/system-packages/brew.md
🚧 Files skipped from review as they are similar to previous changes (34)
- docs/cli/system/status.md
- src/config/config_file/mod.rs
- man/man1/mise.1
- docs/.vitepress/cli_commands.ts
- e2e/cli/test_system_install_apt
- docs/system-packages/dnf.md
- src/cli/system/mod.rs
- e2e/cli/test_system_install_brew_linux
- xtasks/fig/src/mise.ts
- e2e/run_test
- src/cli/doctor/mod.rs
- src/system/mod.rs
- src/system/packages/brew/state.rs
- src/system/packages/mod.rs
- src/cli/system/status.rs
- docs/system-packages/apt.md
- src/main.rs
- src/system/packages/dnf.rs
- settings.toml
- src/system/packages/brew/fetch.rs
- src/system/packages/brew/relocate.rs
- src/system/sudo.rs
- src/system/packages/brew/macho.rs
- src/cli/system/install.rs
- src/system/packages/brew/prefix.rs
- src/system/packages/brew/pour.rs
- src/system/packages/brew/tag.rs
- src/system/packages/brew/api.rs
- src/system/packages/brew/resolve.rs
- src/system/packages/brew/elf.rs
- src/config/config_file/mise_toml.rs
- src/cli/mod.rs
- src/cli/install.rs
- src/system/packages/pacman.rs
…" specs
Per review discussion: entries now look like [tools] entries with a
required manager prefix and a version value —
[system.packages]
"apt:libssl-dev" = "latest"
"apt:curl" = "8.5.0-2ubuntu10"
"brew:postgresql@17" = "latest"
- version pins move out of manager-native raw strings into the value;
each manager renders its native pin syntax at install time (apt
name=version, dnf name-version[-release])
- config merge is now per-key: a project config can override a global
entry's pin instead of accumulating conflicting raw entries
- `mise system install` accepts positional manager:package specs,
installable whether or not they appear in config
- pacman/brew reject pins they cannot satisfy at install time but still
report version mismatches in status
- apt: preserve all installed multiarch versions so bare-name pins are
not order-dependent, and pass `--` before package operands so entries
can never be parsed as apt-get options (CodeRabbit)
- docs: clarify that brew pours bottles without the Homebrew CLI and
that unavailable managers still appear in status/doctor (CodeRabbit)
Co-Authored-By: Claude Fable 5 <[email protected]>
The previous commit regenerated the zsh wrapper with a stale usage-cli 3.3.0 from Homebrew, downgrading the complete-word handling main already had; re-rendered with the mise-pinned 3.5.0, restoring it to match main. Co-Authored-By: Claude Fable 5 <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
e2e/cli/test_system_install_brew_linux (1)
13-29: ⚡ Quick winEnsure cleanup runs even if test assertions fail.
The script lacks error handling: if any
assert_containscall exits due to failure (line 18, 21, 24, or 27), the cleanup at line 29 will not run, leaving/home/linuxbrewbehind. In CI containers or test isolation, this could affect subsequent test runs.Use a
trapto guarantee cleanup:trap 'rm -rf /home/linuxbrew' EXITPlace it after the guard clauses (after line 11) so cleanup runs unconditionally, even if an earlier assertion fails.
🤖 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 `@e2e/cli/test_system_install_brew_linux` around lines 13 - 29, Add an unconditional cleanup trap so /home/linuxbrew is removed even if any assert_contains fails: after the initial guard/setup block and before running assertions (i.e. before the first assert_contains call), register an EXIT trap that runs the same cleanup currently done by the final rm -rf /home/linuxbrew; this ensures the directory is removed regardless of failures during the assert_contains checks or mise system install steps.
🤖 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.
Nitpick comments:
In `@e2e/cli/test_system_install_brew_linux`:
- Around line 13-29: Add an unconditional cleanup trap so /home/linuxbrew is
removed even if any assert_contains fails: after the initial guard/setup block
and before running assertions (i.e. before the first assert_contains call),
register an EXIT trap that runs the same cleanup currently done by the final rm
-rf /home/linuxbrew; this ensures the directory is removed regardless of
failures during the assert_contains checks or mise system install steps.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7b1dd754-c0ef-466f-a3e8-d281e0ba619c
📒 Files selected for processing (26)
completions/_misedocs/cli/index.mddocs/cli/system.mddocs/cli/system/install.mddocs/system-packages/apt.mddocs/system-packages/brew.mddocs/system-packages/dnf.mddocs/system-packages/index.mddocs/system-packages/pacman.mde2e/cli/test_system_install_apte2e/cli/test_system_install_brew_linuxe2e/cli/test_system_statusman/man1/mise.1mise.usage.kdlschema/mise.jsonsrc/cli/install.rssrc/cli/system/install.rssrc/cli/system/status.rssrc/config/config_file/mise_toml.rssrc/system/mod.rssrc/system/packages/apt.rssrc/system/packages/brew/mod.rssrc/system/packages/dnf.rssrc/system/packages/mod.rssrc/system/packages/pacman.rsxtasks/fig/src/mise.ts
✅ Files skipped from review due to trivial changes (5)
- docs/cli/system.md
- docs/system-packages/dnf.md
- docs/cli/index.md
- docs/system-packages/brew.md
- docs/system-packages/apt.md
🚧 Files skipped from review as they are similar to previous changes (9)
- e2e/cli/test_system_status
- e2e/cli/test_system_install_apt
- src/system/packages/brew/mod.rs
- src/config/config_file/mise_toml.rs
- xtasks/fig/src/mise.ts
- src/system/packages/pacman.rs
- src/cli/system/status.rs
- man/man1/mise.1
- src/cli/install.rs
… status/doctor Excluded managers were dropped during aggregation with only a debug log, so their packages silently vanished from `mise system status` and `mise doctor` — unlike OS-unavailable managers, which show as skipped. The exclusion can live in a different config file than the packages, so surface it: status/doctor now report "excluded by the system_packages.managers setting" rows while install and the missing-packages hint still skip them (Bugbot). Also run the brew linux e2e cleanup in an EXIT trap so a failed assertion cannot leave /home/linuxbrew behind and mask later runs via the test guard (CodeRabbit). Co-Authored-By: Claude Fable 5 <[email protected]>
Pours now report through MultiProgressReport like tool installs: one progress job per formula stepping download (byte progress) -> checksum -> extract (byte progress) -> relocate -> codesign -> link, finishing with a checkmark and the poured version. Replaces the single static "brew: pouring x" log line; downloads and extraction were previously silent. Also include only managers that are actually checked (available, not excluded by system_packages.managers) in the mise install hint fingerprint, so widening the manager filter re-checks immediately instead of waiting out the 24h throttle (Bugbot). Co-Authored-By: Claude Fable 5 <[email protected]>
…atch pacman and brew can never install a pinned version (Arch repos only carry latest; brew bottles only exist for a formula's current version), so a pinned entry for those managers made `mise system install` bail and block every other package in the batch. Add SystemPackageManager::supports_version_pins() and have the install command skip such pins with a warning — they stay visible as a version mismatch in `mise system status`. Also bail (instead of silently exiting 0) when packages are explicitly requested via manager:package specs and that manager is unavailable on this machine. Co-Authored-By: Claude Fable 5 <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8c1853f. Configure here.
The 'N package(s) already installed' line was computed after pinned entries were filtered out of the missing list, so skipped brew/pacman pins were counted as installed. Co-Authored-By: Claude Fable 5 <[email protected]>

Summary
Adds a new
[system]config namespace for machine-global bootstrapping, starting with declarative system packages:Entries are keyed like
[tools]entries — a requiredmanager:prefix plus the package name — and the value is a version:"latest", or a pin in the manager's native format where supported ("apt:curl" = "8.5.0-2ubuntu10","dnf:bash" = "5.2.26-3.fc40"). Each manager renders pins into its native install syntax (aptname=version, dnfname-version[-release]); pacman/brew can't install pinned versions and reject them at install time while still reportingversion mismatchin status.System packages are deliberately not part of
[tools]: they're unversioned-per-project, machine-global, get no shims, and are installed by the platform package manager (or mise's own Homebrew bottle installer). Keys merge as a union across the config hierarchy — a project config can override a global entry's version pin but not remove entries — and managers unavailable on the current machine are not acted on (status/doctor still surface them), so one config works across platforms. mise never installs them implicitly — only an explicitmise system installdoes (mise installprints a one-time hint;mise doctorreports them, in both text and-Jmodes). Everything is gated behindexperimental.Managers
dpkg-querystate checks (multi-arch aware — all installed architectures are considered, andname:archqualifiers match exactly),apt-get install -y -- <pkgs>(end-of-options marker so config entries can never be parsed as apt flags), automaticapt-get updatein fresh containers.rpm -qstate checks with version/version-release pin matching,dnf install -y,--update→--refresh.pacman -Qstate checks,pacman -S --noconfirm --needed, automatic-Sywhen sync DBs are missing. Pins are status-only (Arch repos only carry the latest version)./opt/homebrew) and Linux x86_64/arm64 (/home/linuxbrew/.linuxbrew). Versioning is expressed in formula names (postgresql@17). Details below.A
system_packages.managerssetting restricts which managers run, for machines where several could apply (and future managers like macports/pkgx).First sudo in the codebase
The Linux managers need root. Elevation lives in a single auditable chokepoint (
src/system/sudo.rs):sudo …with inherited stdio so the normal password prompt works; env vars pass viasudo env K=V …(sudo's env_reset would otherwise drop them)sudo -nprobe; on failure, error with the exact command to run manually — never hangs on a password promptsystem_packages.sudo = falsedisables elevation entirelybrew without brew
mise pours bottles itself — it never shells out to
brew:variations), topo-sorted.brew pourdoes (ported from keg_relocate.rb): placeholder replacement in text files, in-place NUL-padded C-string replacement in binaries, and proper Mach-O load-command rewriting that growscmdsizeinto header padding when the replacement is longer (the@@HOMEBREW_CELLAR@@+1-byte case — hit in the wild by icu4c), equivalent to what ruby-macho does for brew. On Linux, the ELF interpreter and rpath carry the placeholders (growing 19 → 26 bytes when restored), soelf.rsports the patchelf strategy brew uses via its PatchELF gem: strings that no longer fit move into a newPT_LOADsegment appended to the binary along with a relocated program header table; the interpreter points at<prefix>/lib/ld.so, a symlink mise maintains to the host loader (or a brewed glibc). Brew's Linux rules are mirrored exactly::any_skip_relocationhonored only for bottles built by Homebrew ≥ 5.1.15, rpath component filtering +gcc/currentrewrite +<prefix>/libappending, glibc never patched.INSTALL_RECEIPT.jsonwritten into each keg, so a real Homebrew sees mise's kegs as its own —brew list/upgrade/uninstallall work — and mise's status checks count brew-installed formulae as installed. mise additionally keeps its own ledger (atomic writes) of what it installed.optsymlink + prefix linking with brew's overwrite rules (only repoint symlinks into Cellar/opt; anything else is a conflict error, never clobbered; link failure rolls the keg back, restoring any symlinks it replaced). keg-only formulae getoptbut aren't linked.Installing at the canonical prefix is the load-bearing design decision: ~14% of macOS bottles (openssl, ncurses, python, git, ffmpeg, postgres, php — pinning is viral through dep closures) and nearly all Linux bottles only work at the canonical prefix, and those are exactly the shared-library packages people want brew for (#4495, #4560). Relocating into a mise-style prefix is not viable (binary path rewrites can't exceed the canonical prefix length; pkgx reached the same conclusion and built its own packages instead).
One-time sudo creates the prefix when missing (mirroring brew's own install.sh, owner derived from euid); after that everything is user-owned file operations.
Scope: formulae only — no taps (require Ruby), no casks, no services, no Intel macs, no source builds.
Verified end-to-end
arm64 macOS, clean test prefix:
jq(:any, 1 dep): poured, Mach-O install names relocated, codesign valid, binary runspostgresql@17closure (icu4c, krb5, lz4, readline, xz, zstd, openssl@3, ca-certificates): full pour in ~18s; icu4c's packed load commands (the +1-byte growth case) rewritten correctly —makeconv/uconvrun with valid signatureszlib):optlink only; idempotency, status table/JSON, dry-run, ledger, receiptsLinux (Docker, x86_64 and via CI arm64):
xz+jqclosure poured at the real/home/linuxbrew/.linuxbrew— ELF interp/rpath patched, both binaries run, idempotent.Plus 24 unit tests (dpkg/rpm/pacman parsers incl. arch-qualified and multi-arch versioned matching, Mach-O patcher incl. synthetic grow/no-padding cases, ELF patcher, relocation, config parsing) and e2e tests (
test_system_statuseverywhere;test_system_install_aptandtest_system_install_brew_linuxin Linux-root CI).Docs
New section with an overview and a page per manager:
docs/system-packages/{index,apt,dnf,pacman,brew}.md.This PR was written by an AI coding assistant (Claude Code), directed and reviewed by @jdx.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
mise system installandmise system statuswith flags like --manager, --dry-run, --yes, --update, --json, and --missing[system.packages](apt, dnf, pacman, brew)mise doctorintegration and post-install hints for missing system packagesDocumentation
Tests
Shell
Note
High Risk
Large new surface area: sudo elevation, system package mutation, and a full brew bottle pour pipeline (binary relocation/codesign) with platform-specific failure modes.
Overview
Adds experimental declarative OS packages via
[system.packages]inmise.toml, keyed asmanager:packagewith optional version pins, separate from[tools].New
mise systemcommands:status(table/JSON,--missingfor CI) andinstall(config or explicit specs,--manager,--dry-run,--yes,--update). Packages install only on explicitmise system install;mise installmay show a throttled hint, andmise doctorreports missing packages (text and JSON).Managers: apt, dnf, and pacman use native tools with read-only checks and
sudoelevation viasystem_packages.sudoandsystem_packages.managers. brew pours homebrew/core bottles into canonical prefixes without thebrewCLI (relocation, codesign, brew-compatible receipts).Config/schema/docs/man/completions updated; e2e gains Docker tweaks for writable rootfs/
/homeand tests for status, apt install, and Linux brew pour.Reviewed by Cursor Bugbot for commit edbc358. Bugbot is set up for automated code reviews on this repo. Configure here.