diff --git a/docs/dev-tools/backends/cargo.md b/docs/dev-tools/backends/cargo.md
index d9ce5c98d1..620e627cef 100644
--- a/docs/dev-tools/backends/cargo.md
+++ b/docs/dev-tools/backends/cargo.md
@@ -62,6 +62,14 @@ This will execute a `cargo install` command with the corresponding Git options.
Set these with `mise settings set [VARIABLE] [VALUE]` or by setting the environment variable listed.
+Some Cargo settings are only meaningful when mise runs `cargo install`. If `cargo-binstall`
+installs a prebuilt binary, Cargo build settings and `cargo install` behavior do not affect that
+artifact. Set `cargo.binstall = false` when you need Cargo settings to control the install.
+
+When mise uses `cargo-binstall`, mise runs `cargo-binstall` once and lets `cargo-binstall` handle
+its own fallback order, including its final fallback to compiling with `cargo install`. mise does
+not retry with a separate `cargo install` command if `cargo-binstall` exits with an error.
+
@@ -72,6 +80,21 @@ import Settings from '/components/settings.vue';
The following [tool-options](/dev-tools/#tool-options) are available for the `cargo` backend—these
go in `[tools]` in `mise.toml`.
+When `cargo-binstall` is available, mise uses it for registry installs unless a tool option needs
+`cargo install` to build from source.
+
+For options that do not skip `cargo-binstall`, any source-build fallback is handled by
+`cargo-binstall` itself. mise does not perform an additional compile fallback after
+`cargo-binstall` fails.
+
+| Option | `cargo-binstall` behavior |
+| -------------------------- | ---------------------------------------------------------------------------------------- |
+| `features` | Skips `cargo-binstall`; requires `cargo install --features`. |
+| `default-features = false` | Skips `cargo-binstall`; requires `cargo install --no-default-features`. |
+| `bin` | Passed through to `cargo-binstall`; does not skip it. |
+| `crate` | Does not skip `cargo-binstall` when applicable. Git installs always use `cargo install`. |
+| `locked` | Passed through to `cargo-binstall`; does not skip it. |
+
### `features`
Install additional components (passed as `cargo install --features`):
@@ -81,6 +104,8 @@ Install additional components (passed as `cargo install --features`):
"cargo:cargo-edit" = { version = "latest", features = "add" }
```
+This option requires `cargo install`; mise skips `cargo-binstall` when it is set.
+
### `default-features`
Disable default features (passed as `cargo install --no-default-features`):
@@ -90,6 +115,8 @@ Disable default features (passed as `cargo install --no-default-features`):
"cargo:cargo-edit" = { version = "latest", default-features = false }
```
+Setting this to `false` requires `cargo install`; mise skips `cargo-binstall` in that case.
+
### `bin`
Select the CLI bin name to install when multiple are available (passed as `cargo install --bin`):
@@ -99,6 +126,8 @@ Select the CLI bin name to install when multiple are available (passed as `cargo
"cargo:https://github.com/username/demo" = { version = "tag:v1.0.0", bin = "demo" }
```
+This option is supported by `cargo-binstall`, so it does not cause mise to skip `cargo-binstall`.
+
### `crate`
Select the crate name to install when multiple are available (passed as
@@ -109,6 +138,9 @@ Select the crate name to install when multiple are available (passed as
"cargo:https://github.com/username/demo" = { version = "tag:v1.0.0", crate = "demo" }
```
+This option does not cause mise to skip `cargo-binstall` when applicable. Git installs already use
+`cargo install`.
+
### `locked`
Use Cargo.lock (passes `cargo install --locked`) when building CLI. This is the default behavior,
@@ -118,3 +150,6 @@ pass `false` to disable:
[tools]
"cargo:https://github.com/username/demo" = { version = "latest", locked = false }
```
+
+This option does not cause mise to skip `cargo-binstall`; it only affects the install if
+`cargo-binstall` itself falls back to compiling with `cargo install`.
diff --git a/settings.toml b/settings.toml
index b673987d0f..911df6f4ed 100644
--- a/settings.toml
+++ b/settings.toml
@@ -204,6 +204,14 @@ If true, mise will use `cargo binstall` instead of `cargo install` if
[`cargo-binstall`](https://crates.io/crates/cargo-binstall) is installed and on PATH.
This makes installing CLIs with cargo _much_ faster by downloading precompiled binaries.
+When `cargo-binstall` installs a prebuilt binary, Cargo build settings and `cargo install`
+behavior do not affect the downloaded artifact. Set `cargo.binstall = false` to force
+`cargo install` when you need Cargo settings to control the install.
+
+When mise invokes `cargo-binstall`, `cargo-binstall` handles its own fallback order, including
+its final fallback to compiling with `cargo install`. mise does not retry with a separate
+`cargo install` command if `cargo-binstall` exits with an error.
+
You can install it with mise:
```sh
diff --git a/src/backend/cargo.rs b/src/backend/cargo.rs
index 0a9d42289e..b9055d9c8a 100644
--- a/src/backend/cargo.rs
+++ b/src/backend/cargo.rs
@@ -50,37 +50,33 @@ impl<'a> CargoOptions<'a> {
fn locked(&self) -> bool {
self.values
.raw()
- .get("locked")
+ .get_string("locked")
.is_none_or(|v| v.to_lowercase() != "false")
}
- fn features(&self) -> Option<&'a str> {
- self.values.str("features")
+ fn features(&self) -> Option {
+ self.values.raw().get_string("features")
}
fn default_features_disabled(&self) -> bool {
self.values
- .str("default-features")
+ .raw()
+ .get_string("default-features")
.is_some_and(|v| v.to_lowercase() == "false")
}
- fn crate_arg(&self) -> Option<&'a str> {
- self.values.str("crate")
+ fn crate_arg(&self) -> Option {
+ self.values.raw().get_string("crate")
}
fn install_env(&self) -> &'a IndexMap {
&self.values.raw().install_env
}
- fn has_features_options(&self) -> bool {
- self.values.raw().contains_key("features")
- || self.values.raw().contains_key("default-features")
- }
-
fn lockfile_options(&self, target: &PlatformTarget) -> BTreeMap {
let mut result = BTreeMap::new();
- for key in ["features", "default-features"] {
- if let Some(value) = self.values.str(key) {
+ for key in ["features", "default-features", "crate", "locked"] {
+ if let Some(value) = self.values.raw().get_string(key) {
result.insert(key.to_string(), value.to_string());
}
}
@@ -91,6 +87,14 @@ impl<'a> CargoOptions<'a> {
}
}
+#[derive(Debug)]
+enum BinstallStatus {
+ Enabled,
+ Disabled,
+ Unavailable,
+ UnsupportedOptions(Vec<&'static str>),
+}
+
#[async_trait]
impl Backend for CargoBackend {
fn get_type(&self) -> BackendType {
@@ -183,16 +187,39 @@ impl Backend for CargoBackend {
))?;
}
cmd
- } else if self.is_binstall_enabled(&config, &tv).await {
- let mut cmd = CmdLineRunner::new("cargo-binstall").arg("-y");
- if let Some(token) = &*GITHUB_TOKEN {
- cmd = cmd.env("GITHUB_TOKEN", token)
- }
- cmd.arg(install_arg)
- } else if Settings::get().cargo.binstall_only {
- bail!("cargo-binstall is not available, but cargo.binstall_only is set");
} else {
- cmd.arg(install_arg)
+ match self.binstall_status(&config, &tv).await {
+ BinstallStatus::Enabled => {
+ let mut cmd = CmdLineRunner::new("cargo-binstall").arg("-y");
+ if let Some(token) = &*GITHUB_TOKEN {
+ cmd = cmd.env("GITHUB_TOKEN", token)
+ }
+ cmd.arg(install_arg)
+ }
+ BinstallStatus::UnsupportedOptions(options)
+ if Settings::get().cargo.binstall_only =>
+ {
+ let options = format_tool_options(&options);
+ bail!(
+ "cargo-binstall cannot honor cargo install-only tool option(s): {options}\n\
+ hint: Remove the option(s), or disable cargo.binstall_only to allow cargo install"
+ );
+ }
+ BinstallStatus::Disabled if Settings::get().cargo.binstall_only => {
+ bail!("cargo-binstall is disabled, but cargo.binstall_only is set");
+ }
+ _ if Settings::get().cargo.binstall_only => {
+ bail!("cargo-binstall is not available, but cargo.binstall_only is set");
+ }
+ BinstallStatus::UnsupportedOptions(options) => {
+ let options = format_tool_options(&options);
+ info!(
+ "not using cargo-binstall because cargo install-only tool option(s) are specified: {options}"
+ );
+ cmd.arg(install_arg)
+ }
+ _ => cmd.arg(install_arg),
+ }
};
let request_options = tv.request.options();
@@ -245,7 +272,13 @@ impl Backend for CargoBackend {
/// Returns install-time-only option keys for Cargo backend.
pub fn install_time_option_keys() -> Vec {
- vec!["features".into(), "default-features".into(), "bin".into()]
+ vec![
+ "features".into(),
+ "default-features".into(),
+ "bin".into(),
+ "crate".into(),
+ "locked".into(),
+ ]
}
impl CargoBackend {
@@ -253,29 +286,45 @@ impl CargoBackend {
Self { ba: Arc::new(ba) }
}
- async fn is_binstall_enabled(&self, config: &Arc, tv: &ToolVersion) -> bool {
+ fn cargo_install_required_options(opts: &ToolVersionOptions) -> Vec<&'static str> {
+ let mut options = vec![];
+ if opts
+ .get_string("features")
+ .is_some_and(|features| !features.trim().is_empty())
+ {
+ options.push("features");
+ }
+ if opts
+ .get_string("default-features")
+ .is_some_and(|default_features| default_features.to_lowercase() == "false")
+ {
+ options.push("default-features");
+ }
+ options
+ }
+
+ async fn binstall_status(&self, config: &Arc, tv: &ToolVersion) -> BinstallStatus {
if !Settings::get().cargo.binstall {
- return false;
+ return BinstallStatus::Disabled;
+ }
+ let opts = tv.request.options();
+ let cargo_install_required_options = Self::cargo_install_required_options(&opts);
+ if !cargo_install_required_options.is_empty() {
+ return BinstallStatus::UnsupportedOptions(cargo_install_required_options);
}
if file::which_non_pristine("cargo-binstall").is_none() {
match self.dependency_toolset(config).await {
Ok(ts) => {
if ts.which(config, "cargo-binstall").await.is_none() {
- return false;
+ return BinstallStatus::Unavailable;
}
}
Err(_e) => {
- return false;
+ return BinstallStatus::Unavailable;
}
}
}
- let request_options = tv.request.options();
- let opts = CargoOptions::new(&request_options);
- if opts.has_features_options() {
- info!("not using cargo-binstall because features are specified");
- return false;
- }
- true
+ BinstallStatus::Enabled
}
/// if the name is a git repo, return the git url
@@ -290,6 +339,14 @@ impl CargoBackend {
}
}
+fn format_tool_options(options: &[&'static str]) -> String {
+ options
+ .iter()
+ .map(|option| format!("`{option}`"))
+ .collect::>()
+ .join(", ")
+}
+
#[derive(Debug, serde::Deserialize)]
struct CratesIoVersionsResponse {
versions: Vec,
@@ -306,6 +363,7 @@ struct CratesIoVersion {
mod tests {
use super::*;
use crate::platform::Platform;
+ use crate::toolset::parse_tool_options;
#[test]
fn test_lockfile_options_uses_target_platform_bin() {
@@ -324,4 +382,52 @@ mod tests {
assert_eq!(lock_opts.get("bin").map(String::as_str), Some("linux-bin"));
}
+
+ #[test]
+ fn test_lockfile_options_include_crate_and_locked() {
+ let mut opts = ToolVersionOptions::default();
+ opts.opts
+ .insert("crate".into(), toml::Value::String("demo".into()));
+ opts.opts
+ .insert("locked".into(), toml::Value::Boolean(false));
+
+ let target = PlatformTarget::new(Platform::parse("linux-x64").unwrap());
+ let lock_opts = CargoOptions::new(&opts).lockfile_options(&target);
+
+ assert_eq!(lock_opts.get("crate").map(String::as_str), Some("demo"));
+ assert_eq!(lock_opts.get("locked").map(String::as_str), Some("false"));
+ }
+
+ #[test]
+ fn cargo_install_required_options_skips_feature_options() {
+ let opts = parse_tool_options("features=add,default-features=false");
+
+ assert_eq!(
+ CargoBackend::cargo_install_required_options(&opts),
+ vec!["features", "default-features"]
+ );
+ }
+
+ #[test]
+ fn cargo_install_required_options_allows_binstall_supported_options() {
+ let opts =
+ parse_tool_options("bin=cargo-add,crate=cargo-edit,locked=false,default-features=true");
+
+ assert_eq!(
+ CargoBackend::cargo_install_required_options(&opts),
+ Vec::<&str>::new()
+ );
+ }
+
+ #[test]
+ fn cargo_install_required_options_skips_toml_bool_default_features() {
+ let mut opts = ToolVersionOptions::default();
+ opts.opts
+ .insert("default-features".into(), toml::Value::Boolean(false));
+
+ assert_eq!(
+ CargoBackend::cargo_install_required_options(&opts),
+ vec!["default-features"]
+ );
+ }
}