Skip to content

feat: add arch filter for tool version options#9073

Closed
webkaz wants to merge 2 commits into
jdx:mainfrom
webkaz:feat/arch-filter
Closed

feat: add arch filter for tool version options#9073
webkaz wants to merge 2 commits into
jdx:mainfrom
webkaz:feat/arch-filter

Conversation

@webkaz

@webkaz webkaz commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an arch filter to [tools] entries, complementing the existing os filter:

[tools]
"aqua:sharkdp/fd" = { version = "latest", arch = ["arm64"] }
"cargo:fd-find" = { version = "latest", arch = ["x64"] }

Use case from #8948: some prebuilt binaries only ship for arm64 on macOS, so dotfiles managed across Intel and ARM Macs need to fall back to a different backend per architecture. Previously this required external templating (e.g., chezmoi); now it's native.

When both os and arch are specified, both must match. Accepted values mirror crate::cli::version::ARCH (x64 / arm64); other std::env::consts::ARCH values pass through verbatim.

This is the implementation for #8948 ("Seems fine to me" — @jdx).

Implementation

  • New arch: Option<Vec<String>> field on ToolVersionOptions, included in the manual Hash impl
  • New arch() accessor on ToolRequest, parallel to os()
  • Filter logic split out into a private matches_current_platform() so unit tests can exercise the filter without touching the install-state singleton that BackendArg::is_os_supported consults
  • mise_toml.rs deserializers (both MiseTomlToolList and MiseTomlTool) gain an "arch" => arm; serializer writes arch next to os; output_empty_opts and to_tool_request_set updated to include arch
  • tool_stub.rs exposes the same arch field on ToolStubFile so stubs can use it
  • Documentation: new "Architecture-Specific Tools" section in docs/dev-tools/index.md, os/arch listed under Optional Fields in docs/dev-tools/tool-stubs.md
  • Snapshots regenerated for the new field

Test plan

  • Unit tests added: 5 new tool_request filter tests + snapshot updates for mise_toml. Full suite passes locally (633/633)
  • CI

Closes #8948


This PR was prepared with Claude Code.

Mirrors the existing `os` field on `ToolVersionOptions`, allowing tools
to be restricted to specific CPU architectures via:

    [tools]
    "aqua:sharkdp/fd" = { version = "latest", arch = ["arm64"] }
    "cargo:fd-find" = { version = "latest", arch = ["x64"] }

When both `os` and `arch` are specified, both must match the current
platform for the tool to be active. Accepted values mirror
`crate::cli::version::ARCH` (which normalizes `x86_64` to `x64` and
`aarch64` to `arm64`); other `std::env::consts::ARCH` values pass
through verbatim.

Implementation:
- New `arch: Option<Vec<String>>` field on `ToolVersionOptions` (next to
  `os`), included in the manual `Hash` impl.
- New `arch()` accessor on `ToolRequest`, parallel to `os()`.
- Filter logic split out into a private `matches_current_platform()` so
  unit tests can exercise `os`/`arch` without the install-state
  singleton that `BackendArg::is_os_supported` requires.
- Custom `mise_toml.rs` deserializers gain an `"arch" =>` arm in both
  `MiseTomlToolList` and `MiseTomlTool` visitors, mirroring `os`.
- `replace_versions` writes `arch` into the inline table next to `os`,
  and `output_empty_opts` accounts for `arch` when deciding whether to
  emit an inline table at all.
- `to_tool_request_set` copies `arch` from config onto cached backend
  options alongside `os`/`depends`/`install_env`.
- `tool_stub.rs` exposes the same `arch` field on `ToolStubFile` so
  stubs can use it.
- Snapshots regenerated to include the new field.
- Documentation updated in `docs/dev-tools/index.md` (new
  Architecture-Specific Tools section) and `docs/dev-tools/tool-stubs.md`
  (Optional Fields).

Closes jdx#8948
Copilot AI review requested due to automatic review settings April 13, 2026 17:58
@greptile-apps

greptile-apps Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an arch filter to [tools] entries in mise.toml (and tool stubs), mirroring the existing os filter so users can target specific CPU architectures. It also fixes a pre-existing bug where os (and now arch) were silently dropped from the TOML serializer when a tool has multiple versions listed — those fields were never re-written by replace_versions's multi-version code path, meaning edits via mise use would lose the filter.

Confidence Score: 5/5

Safe to merge — the previous scalar-bypass bug is fixed, filter logic is correct, and all code paths are covered by tests.

No P0 or P1 issues remain. The implementation is a clean parallel of the existing os filter, the scalar arch deserialization bug raised in the prior review has been addressed, and the PR incidentally fixes a pre-existing os/install_env serialization bug in the multi-version code path. Unit tests cover all meaningful combinations of the filter logic.

No files require special attention.

Important Files Changed

Filename Overview
src/toolset/tool_version_options.rs Adds arch: Option<Vec<String>> field with correct Hash impl and fixes is_empty() to include both os and arch checks (previously os was not checked).
src/toolset/tool_request.rs Adds arch() accessor, refactors filter logic into matches_current_platform() for testability, and adds 4 well-targeted unit tests covering all filter combinations.
src/config/config_file/mise_toml.rs Deserializers in both MiseTomlToolList and MiseTomlTool correctly handle scalar and array arch; also fixes pre-existing bug where os/install_env were dropped in the multi-version serializer path.
src/cli/tool_stub.rs Adds arch field, adds "arch" to the skip list in the generic option deserializer, and copies it into ToolVersionOptions correctly.
docs/dev-tools/index.md Adds clear 'Architecture-Specific Tools' section with accurate examples, accepted values, and interaction with os.
docs/dev-tools/tool-stubs.md Documents os and arch optional fields for tool stubs with correct cross-references.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["mise.toml / tool stub"] -->|"arch = [...]"| B["MiseTomlToolList / MiseTomlTool\ndeserializer"]
    B -->|"options.arch = Some(vec![...])"| C["ToolVersionOptions"]
    C --> D["to_tool_request_set\nba_opts.arch = options.arch.clone()"]
    D --> E["ToolRequest\n(arch field in options)"]
    E --> F["is_os_supported()"]
    F --> G["matches_current_platform()"]
    G -->|"arch filter"| H{"arch.contains(ARCH)?"}
    H -->|"yes / arch=None"| I["ba().is_os_supported()"]
    H -->|"no"| J["skip tool\n(not installed/used)"]
    I -->|"true"| K["Tool active on this platform"]
    I -->|"false"| J
Loading

Reviews (2): Last reviewed commit: "fix: address review feedback for arch fi..." | Re-trigger Greptile

Comment thread src/config/config_file/mise_toml.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ae2bf20d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/config/config_file/mise_toml.rs Outdated
Comment on lines +1585 to +1588
toml::Value::String(s) => {
// Convert {{version}} to {version} for backend templating
let s = s.replace("{{version}}", "{version}");
options.opts.insert(k, toml::Value::String(s));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse arch string values as platform filters

When arch is provided as a string, this branch stores it in options.opts instead of options.arch, so ToolRequest::arch() stays None and the architecture filter is never enforced. In practice, configs like arch = "arm64" are silently treated as a backend option rather than a platform gate, which can install/use tools on unsupported architectures.

Useful? React with 👍 / 👎.

Comment on lines +574 to 575
if opts.os.is_some() || opts.arch.is_some() || !opts.install_env.is_empty() {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve arch when rewriting multi-version tool entries

Adding opts.arch.is_some() here forces arch-restricted requests down the non-empty serialization path, but the multi-version branch only writes version and opts, not arch. As a result, running replace_versions on a tool with multiple versions and an arch filter will drop the filter from mise.toml, changing behavior on subsequent runs.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements architecture-specific tool filtering by adding an arch field to tool configurations and stubs. The changes include updating the ToolVersionOptions structure, enhancing the ToolRequest platform matching logic, and providing relevant documentation. Review feedback highlighted several issues: the deserialization logic incorrectly handles single-string architecture values by placing them in a generic options map, and the use of unwrap() poses a panic risk. Furthermore, the is_empty method in ToolVersionOptions requires an update to include the new fields, ensuring that platform filters are not lost during configuration serialization.

Comment on lines +1579 to +1593
"arch" => match v {
toml::Value::Array(s) => {
options.arch = Some(
s.iter().map(|v| v.as_str().unwrap().to_string()).collect(),
);
}
toml::Value::String(s) => {
// Convert {{version}} to {version} for backend templating
let s = s.replace("{{version}}", "{version}");
options.opts.insert(k, toml::Value::String(s));
}
_ => {
return Err(de::Error::custom("arch must be a string or array"));
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When arch is provided as a single string, it is currently being inserted into the generic opts map instead of the dedicated arch field. This causes the architecture filter to be ignored by ToolRequest::is_os_supported(). Additionally, the array handling uses unwrap(), which will panic if the array contains non-string values. It should follow the safer pattern used by the depends field.

                        "arch" => match v {
                            toml::Value::Array(arr) => {
                                options.arch = Some(
                                    arr.iter()
                                        .map(|v| {
                                            v.as_str()
                                                .ok_or_else(|| de::Error::custom("arch array must contain only strings"))
                                                .map(|s| s.to_string())
                                        })
                                        .collect::<Result<Vec<_>, _>>()?,
                                );
                            }
                            toml::Value::String(s) => {
                                options.arch = Some(vec![s]);
                            }
                            _ => {
                                return Err(de::Error::custom("arch must be a string or array"));
                            }
                        },

Comment on lines +1734 to +1748
"arch" => match v {
toml::Value::Array(s) => {
options.arch = Some(
s.iter().map(|v| v.as_str().unwrap().to_string()).collect(),
);
}
toml::Value::String(s) => {
// Convert {{version}} to {version} for backend templating
let s = s.replace("{{version}}", "{version}");
options.opts.insert(k, toml::Value::String(s));
}
_ => {
return Err(de::Error::custom("arch must be a string or array"));
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When arch is provided as a single string, it is currently being inserted into the generic opts map instead of the dedicated arch field. This causes the architecture filter to be ignored by ToolRequest::is_os_supported(). Additionally, the array handling uses unwrap(), which will panic if the array contains non-string values. It should follow the safer pattern used by the depends field.

                        "arch" => match v {
                            toml::Value::Array(arr) => {
                                options.arch = Some(
                                    arr.iter()
                                        .map(|v| {
                                            v.as_str()
                                                .ok_or_else(|| de::Error::custom("arch array must contain only strings"))
                                                .map(|s| s.to_string())
                                        })
                                        .collect::<Result<Vec<_>, _>>()?,
                                );
                            }
                            toml::Value::String(s) => {
                                options.arch = Some(vec![s]);
                            }
                            _ => {
                                return Err(de::Error::custom("arch must be a string or array"));
                            }
                        },

#[derive(Debug, Default, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ToolVersionOptions {
pub os: Option<Vec<String>>,
pub arch: Option<Vec<String>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The is_empty method in ToolVersionOptions (around line 67) must be updated to include the new arch field (and the existing os field). Currently, if a tool entry only contains os or arch filters, is_empty() will return true. This causes MiseTomlTool::from to set the options to None, leading to the filters being dropped when the configuration is processed or re-saved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an arch platform filter to tool entries (alongside existing os) so tools can be conditionally enabled/disabled per CPU architecture, including support in config parsing/serialization, tool stubs, docs, and tests.

Changes:

  • Introduces arch: Option<Vec<String>> in ToolVersionOptions and exposes it via ToolRequest::arch(), with filtering logic in ToolRequest.
  • Updates mise.toml read/write handling to parse/emit arch, plus updates snapshots.
  • Extends tool stub schema/docs to support arch, and adds unit tests for the new filter behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/toolset/tool_version_options.rs Adds arch to tool options and includes it in hashing.
src/toolset/tool_request.rs Exposes arch() accessor and applies os+arch matching in platform support checks; adds tests.
src/config/config_file/mise_toml.rs Parses/serializes arch in mise.toml tool definitions and updates option handling.
src/cli/tool_stub.rs Adds arch support to tool stub files and option deserialization.
docs/dev-tools/tool-stubs.md Documents os/arch optional fields for tool stubs.
docs/dev-tools/index.md Adds “Architecture-Specific Tools” section documenting arch.
src/config/config_file/snapshots/*.snap Snapshot updates reflecting the new arch field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 10 to 16
#[derive(Debug, Default, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ToolVersionOptions {
pub os: Option<Vec<String>>,
pub arch: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub install_env: IndexMap<String, String>,
#[serde(flatten)]

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolVersionOptions::is_empty() currently ignores the platform filter fields. With the new arch field (and existing os), option sets that only contain os/arch will still be treated as “empty”, which can cause filters to be dropped in places that rely on is_empty() (e.g., omitting options when serializing a MiseTomlTool). Update is_empty() (or introduce a separate helper) so it accounts for non-empty os/arch filters.

Copilot uses AI. Check for mistakes.
Comment thread src/config/config_file/mise_toml.rs Outdated
"arch" => match v {
toml::Value::Array(s) => {
options.arch = Some(
s.iter().map(|v| v.as_str().unwrap().to_string()).collect(),

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the arch array deserialization, v.as_str().unwrap() will panic if the TOML array contains a non-string (e.g., arch = [1]). This should return a structured deserialization error instead of crashing. Consider using as_str().ok_or_else(...) and collecting into a Result<Vec<_>, _> (similar to the depends parsing below).

Suggested change
s.iter().map(|v| v.as_str().unwrap().to_string()).collect(),
s.iter()
.map(|v| {
v.as_str()
.ok_or_else(|| de::Error::custom("arch array must contain only strings"))
.map(|s| s.to_string())
})
.collect::<Result<Vec<_>, _>>()?,

Copilot uses AI. Check for mistakes.
Comment thread src/config/config_file/mise_toml.rs Outdated
Comment on lines +1735 to +1739
toml::Value::Array(s) => {
options.arch = Some(
s.iter().map(|v| v.as_str().unwrap().to_string()).collect(),
);
}

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above in MiseTomlTool deserialization: v.as_str().unwrap() inside the arch array branch can panic on non-string array members. Prefer validating each element and returning a de::Error rather than unwrapping.

Copilot uses AI. Check for mistakes.
Comment on lines 573 to 577
let output_empty_opts = |opts: &ToolVersionOptions| {
if opts.os.is_some() || !opts.install_env.is_empty() {
if opts.os.is_some() || opts.arch.is_some() || !opts.install_env.is_empty() {
return false;
}
if let Some(reg_ba) = REGISTRY.get(ba.short.as_str()).and_then(|b| b.ba())

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace_versions now treats arch as a non-empty option (via output_empty_opts), but in the versions.len() > 1 serialization path only tr.options().opts is written into the inline table—named fields like os/arch/depends/install_env are omitted. This means running commands that call replace_versions can silently drop arch (and existing os, etc.) filters for multi-version tool entries. The multi-version table serialization should include these named option fields as well (similar to the single-version branch).

Copilot uses AI. Check for mistakes.
@risu729

risu729 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

I think it's better to support tools only supported on Linux and Windows, but only for x64. Some tools lack precompiled binaries only for Windows arm64.
I'm not sure if this is good syntax for mise, but Aqua supports supported-envs.
https://aquaproj.github.io/docs/reference/registry-config/supported-envs

It might be better to wait for jdx's decision. Current implementation could be fine.

Four fixes based on automated code review:

1. **String arch/os handling**: `arch = "arm64"` (without brackets) was
   silently absorbed into the generic opts map instead of populating
   options.arch. Now stores it as Some(vec![s]), matching depends.
   Same fix applied to the os field for consistency.

2. **Multi-version serialization**: replace_versions only wrote named
   fields (os/arch/install_env) for single-version entries. Multi-
   version inline tables now include them as well, preventing filter
   loss on re-save.

3. **Safe array parsing**: arch/os array deserialization used unwrap()
   which panics on non-string elements. Replaced with the
   ok_or_else/collect::<Result> pattern used by depends.

4. **is_empty() now includes os/arch**: Previously, tool entries with
   only os or arch filters were treated as "empty", causing
   MiseTomlTool::from to set options to None and drop the filters.
@webkaz

webkaz commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

All review points have been addressed in the latest commit (2953bd9):

  1. Scalar arch/os handling (greptile, codex, gemini-code-assist): Fixed — arch = "arm64" (string without brackets) now stores Some(vec![s]) instead of falling into opts. Same fix applied to os for consistency.
  2. Multi-version serialization (codex, Copilot): Fixed — replace_versions multi-version path now writes os, arch, and install_env into inline tables, preventing filter loss on re-save.
  3. Safe array parsing (Copilot, gemini-code-assist): Fixed — unwrap() replaced with ok_or_else/collect::<Result> pattern (matching depends), in both MiseTomlToolList and MiseTomlTool visitors.
  4. is_empty() update (gemini-code-assist, Copilot): Fixed — now includes os.is_none() && arch.is_none() to prevent filter loss when only platform filters are set.

This comment was generated by Claude Code.

@webkaz

webkaz commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@risu729 Good point about the cross-product case (e.g. "Linux and Windows, but only x64"). The current os + arch design handles it as independent filters — both must match — which covers common use cases like "arm64 only" or "linux only", but can't express "x64 on linux + any arch on macos" in a single entry.

Aqua's supported-envs approach (explicit os/arch pairs like linux/amd64) is more expressive for those cases. That said, the separate-field approach was chosen to mirror the existing os filter syntax and keep backwards compatibility. If @jdx prefers the combined approach, happy to rework.

This comment was generated by Claude Code.

@webkaz

webkaz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Closing — the os/arch compound syntax in #9088 and schema support in #9095 cover this use case. Thanks @RobertDeRose and @risu729!

@webkaz webkaz closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants