Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/dev-tools/backends/cargo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<script setup>
import Settings from '/components/settings.vue';
</script>
Expand All @@ -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`):
Expand All @@ -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`):
Expand All @@ -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`):
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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`.
8 changes: 8 additions & 0 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
174 changes: 140 additions & 34 deletions src/backend/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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<String> {
self.values.raw().get_string("crate")
}

fn install_env(&self) -> &'a IndexMap<String, String> {
&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<String, String> {
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());
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -245,37 +272,59 @@ impl Backend for CargoBackend {

/// Returns install-time-only option keys for Cargo backend.
pub fn install_time_option_keys() -> Vec<String> {
vec!["features".into(), "default-features".into(), "bin".into()]
vec![
"features".into(),
"default-features".into(),
"bin".into(),
"crate".into(),
"locked".into(),
]
Comment thread
risu729 marked this conversation as resolved.
}

impl CargoBackend {
pub fn from_arg(ba: BackendArg) -> Self {
Self { ba: Arc::new(ba) }
}

async fn is_binstall_enabled(&self, config: &Arc<Config>, 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
}
Comment thread
risu729 marked this conversation as resolved.

async fn binstall_status(&self, config: &Arc<Config>, tv: &ToolVersion) -> BinstallStatus {
if !Settings::get().cargo.binstall {
return false;
return BinstallStatus::Disabled;
}
Comment thread
risu729 marked this conversation as resolved.
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);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
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
Expand All @@ -290,6 +339,14 @@ impl CargoBackend {
}
}

fn format_tool_options(options: &[&'static str]) -> String {
options
.iter()
.map(|option| format!("`{option}`"))
.collect::<Vec<_>>()
.join(", ")
}

#[derive(Debug, serde::Deserialize)]
struct CratesIoVersionsResponse {
versions: Vec<CratesIoVersion>,
Expand All @@ -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() {
Expand All @@ -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"]
);
}
}
Loading