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"] + ); + } }