diff --git a/docs/lang/rust.md b/docs/lang/rust.md
index 273c06fd36..5328ca9960 100644
--- a/docs/lang/rust.md
+++ b/docs/lang/rust.md
@@ -1,51 +1,40 @@
# Rust
-Rust is not currently offered as a core plugin. In fact, I don't think you
-should actually use mise for rust development. Rust has an official version
-manager called [`rustup`](https://rustup.rs/) that is better than what any of
-the current mise plugins offer.
+Rust/cargo can be installed which uses rustup under the hood. mise will install rustup if it is not
+already installed and add the requested targets. By default, mise uses the default location of rustup/cargo
+(`~/.rustup` and `~/.cargo`), but you can change this by setting the `MISE_RUSTUP_HOME` and `MISE_CARGO_HOME`
+environment variables if you'd like to isolate mise's rustup/cargo from your other rustup/cargo installations.
-You install [rustup](https://rustup.rs/) with the following:
+Unlike most tools, these won't exist inside of `~/.local/share/mise/installs` because they are managed by rustup.
+All mise does is set the `RUST_TOOLCHAIN` environment variable to the requested version and rustup will
+automatically install it if it doesn't exist.
-```sh
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-```
-
-That said, rust is still one of the most popular languages to use in mise.
-A lot of users have success with it so if you'd like to keep all of your
-languages configured the same, don't feel like using mise is a bad idea either. Especially if you're only a casual rust user.
+## Usage
-If you're a relatively heavy rust user making use of things like channel
-overrides, components, and cross-compiling, then I think you really should
-just be using rustup though. The experience will be better.
+Use the latest stable version of rust:
-If one day we could figure out a way to provide an equivalent experience with
-mise, we could revisit this. We have discussed potentially using mise as a
-"front-end" to rustup where there is one rustup install that mise just manages
-so you could do something like this:
-
-```toml
-[tools]
-rust = "nightly"
+```sh
+mise use -g rust
+cargo build
```
-Where that would basically be equivalent to:
+Use the latest beta version of rust:
```sh
-rustup override set nightly
+mise use -g rust@beta
+cargo build
```
-Frankly though, this isn't high on my priority list. Use rustup. It's great.
+Use a specific version of rust:
-Kudos for writing rust too btw, I've really enjoyed it so far—this is my first rust project.
-
-## Default crates
+```sh
+mise use -g rust@1.82
+cargo build
+```
-mise can automatically install a default set of creates right after installing a new rust version.
-To enable this feature, provide a `$HOME/.default-cargo-crates` file that lists one crate per line, for
-example:
+## Settings
-```text
-cargo-edit
-stylua
-```
+
+
diff --git a/docs/registry.md b/docs/registry.md
index acfc8ea24c..6b0ded263a 100644
--- a/docs/registry.md
+++ b/docs/registry.md
@@ -609,7 +609,7 @@ editLink: false
| rstash | [asdf:carlduevel/asdf-rstash](https://github.com/carlduevel/asdf-rstash) |
| ruby | [core:ruby](https://mise.jdx.dev/lang/ruby.html) |
| ruff | [ubi:astral-sh/ruff](https://github.com/astral-sh/ruff) [asdf:simhem/asdf-ruff](https://github.com/simhem/asdf-ruff) |
-| rust | [asdf:code-lever/asdf-rust](https://github.com/code-lever/asdf-rust) |
+| rust | [core:rust](https://mise.jdx.dev/lang/rust.html) [asdf:code-lever/asdf-rust](https://github.com/code-lever/asdf-rust) |
| rust-analyzer | [aqua:rust-lang/rust-analyzer](https://github.com/rust-lang/rust-analyzer) [asdf:Xyven1/asdf-rust-analyzer](https://github.com/Xyven1/asdf-rust-analyzer) |
| rustic | [ubi:rustic-rs/rustic](https://github.com/rustic-rs/rustic) |
| rye | [aqua:astral-sh/rye](https://github.com/astral-sh/rye) [asdf:Azuki-bar/asdf-rye](https://github.com/Azuki-bar/asdf-rye) |
diff --git a/e2e/plugins/core/test_rust b/e2e/plugins/core/test_rust
new file mode 100644
index 0000000000..9be09ac30e
--- /dev/null
+++ b/e2e/plugins/core/test_rust
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+export MISE_RUSTUP_HOME="$MISE_DATA_DIR/rustup"
+export MISE_CARGO_HOME="$MISE_DATA_DIR/cargo"
+
+assert_contains "mise x rust@1.82.0 -- rustc --version" "rustc 1.82.0"
diff --git a/registry.toml b/registry.toml
index 4aed2626e5..e439dd8105 100644
--- a/registry.toml
+++ b/registry.toml
@@ -768,7 +768,7 @@ rome.backends = ["asdf:kichiemon/asdf-rome"]
rstash.backends = ["asdf:carlduevel/asdf-rstash"]
ruby.backends = ["core:ruby"]
ruff.backends = ["ubi:astral-sh/ruff", "asdf:simhem/asdf-ruff"]
-rust.backends = ["asdf:code-lever/asdf-rust"]
+rust.backends = ["core:rust", "asdf:code-lever/asdf-rust"]
rust-analyzer.backends = ["aqua:rust-lang/rust-analyzer", "asdf:Xyven1/asdf-rust-analyzer"]
rustic.backends = ["ubi:rustic-rs/rustic"]
rye.backends = ["aqua:astral-sh/rye", "asdf:Azuki-bar/asdf-rye"]
diff --git a/rustup/settings.toml b/rustup/settings.toml
new file mode 100644
index 0000000000..1551bc97a8
--- /dev/null
+++ b/rustup/settings.toml
@@ -0,0 +1,4 @@
+profile = "default"
+version = "12"
+
+[overrides]
diff --git a/schema/mise.json b/schema/mise.json
index 44a7cdde95..8f2ff851d1 100644
--- a/schema/mise.json
+++ b/schema/mise.json
@@ -539,6 +539,19 @@
}
}
},
+ "rust": {
+ "additionalProperties": false,
+ "properties": {
+ "cargo_home": {
+ "description": "Path to the cargo home directory. Defaults to ~/.cargo or %USERPROFILE%\\.cargo",
+ "type": "string"
+ },
+ "rustup_home": {
+ "description": "Path to the rustup home directory. Defaults to ~/.rustup or %USERPROFILE%\\.rustup",
+ "type": "string"
+ }
+ }
+ },
"shorthands_file": {
"description": "Path to a file containing custom tool shorthands.",
"type": "string"
diff --git a/settings.toml b/settings.toml
index 9ab8936f86..8e00c6aa85 100644
--- a/settings.toml
+++ b/settings.toml
@@ -698,6 +698,18 @@ type = "Bool"
optional = true
description = "Set to true to enable verbose output during ruby installation."
+[rust.cargo_home]
+env = "MISE_CARGO_HOME"
+type = "Path"
+optional = true
+description = "Path to the cargo home directory. Defaults to ~/.cargo or %USERPROFILE%\\.cargo"
+
+[rust.rustup_home]
+env = "MISE_RUSTUP_HOME"
+type = "Path"
+optional = true
+description = "Path to the rustup home directory. Defaults to ~/.rustup or %USERPROFILE%\\.rustup"
+
[shorthands_file]
env = "MISE_SHORTHANDS_FILE"
type = "Path"
diff --git a/src/file.rs b/src/file.rs
index e67c1d5d53..d482954090 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -403,8 +403,6 @@ pub fn make_executable>(path: P) -> Result<()> {
#[cfg(windows)]
pub fn make_executable>(path: P) -> Result<()> {
- trace!("chmod +x {}", display_path(&path));
- warn!("make executable is not available on Windows, use windows_executable_extensions settings instead");
Ok(())
}
diff --git a/src/plugins/core/mod.rs b/src/plugins/core/mod.rs
index c8a2110c92..5f44a60cea 100644
--- a/src/plugins/core/mod.rs
+++ b/src/plugins/core/mod.rs
@@ -18,6 +18,7 @@ use crate::plugins::core::go::GoPlugin;
use crate::plugins::core::java::JavaPlugin;
use crate::plugins::core::node::NodePlugin;
use crate::plugins::core::ruby::RubyPlugin;
+use crate::plugins::core::rust::RustPlugin;
#[cfg(unix)]
use crate::plugins::core::zig::ZigPlugin;
use crate::timeout::run_with_timeout;
@@ -33,6 +34,7 @@ mod node;
mod python;
#[cfg_attr(windows, path = "ruby_windows.rs")]
mod ruby;
+mod rust;
#[cfg(unix)]
mod zig;
@@ -47,6 +49,7 @@ pub static CORE_PLUGINS: Lazy = Lazy::new(|| {
Arc::new(NodePlugin::new()),
Arc::new(PythonPlugin::new()),
Arc::new(RubyPlugin::new()),
+ Arc::new(RustPlugin::new()),
Arc::new(ZigPlugin::new()),
];
#[cfg(windows)]
@@ -59,6 +62,7 @@ pub static CORE_PLUGINS: Lazy = Lazy::new(|| {
Arc::new(NodePlugin::new()),
Arc::new(PythonPlugin::new()),
Arc::new(RubyPlugin::new()),
+ Arc::new(RustPlugin::new()),
// Arc::new(ZigPlugin::new()),
];
plugins
diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs
new file mode 100644
index 0000000000..e7646b455f
--- /dev/null
+++ b/src/plugins/core/rust.rs
@@ -0,0 +1,176 @@
+use std::collections::BTreeMap;
+use std::path::{Path, PathBuf};
+
+use eyre::Result;
+
+use crate::backend::Backend;
+use crate::cli::args::BackendArg;
+use crate::cmd::CmdLineRunner;
+use crate::config::{Config, CONFIG, SETTINGS};
+use crate::http::HTTP;
+use crate::install_context::InstallContext;
+use crate::toolset::{ToolVersion, Toolset};
+use crate::{dirs, file, github, plugins};
+
+#[derive(Debug)]
+pub struct RustPlugin {
+ ba: BackendArg,
+}
+
+impl RustPlugin {
+ pub fn new() -> Self {
+ Self {
+ ba: plugins::core::new_backend_arg("rust"),
+ }
+ }
+
+ fn setup_rustup(&self, ctx: &InstallContext) -> Result<()> {
+ if rustup_home().join("settings.toml").exists() && cargo_bin().exists() {
+ return Ok(());
+ }
+ ctx.pr.set_message("Downloading rustup-init".into());
+ HTTP.download_file(
+ "https://sh.rustup.rs",
+ &rustup_path(),
+ Some(ctx.pr.as_ref()),
+ )?;
+ file::make_executable(rustup_path())?;
+ file::create_dir_all(rustup_home())?;
+ let cmd = CmdLineRunner::new(rustup_path())
+ .with_pr(ctx.pr.as_ref())
+ .arg("--no-modify-path")
+ .arg("--default-toolchain")
+ .arg("none")
+ .arg("-y")
+ .env("RUSTUP_HOME", rustup_home())
+ .env("CARGO_HOME", cargo_home());
+ cmd.execute()?;
+ Ok(())
+ }
+
+ fn test_rust(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
+ ctx.pr.set_message(format!("{RUSTC_BIN} -V"));
+ CmdLineRunner::new(rustc_bin())
+ .with_pr(ctx.pr.as_ref())
+ .arg("-V")
+ .envs(self.exec_env(&CONFIG, CONFIG.get_toolset()?, tv)?)
+ .execute()
+ }
+}
+
+impl Backend for RustPlugin {
+ fn ba(&self) -> &BackendArg {
+ &self.ba
+ }
+
+ fn _list_remote_versions(&self) -> Result> {
+ let versions = github::list_releases("rust-lang/rust")?
+ .into_iter()
+ .map(|r| r.tag_name)
+ .rev()
+ .chain(vec!["nightly".into(), "beta".into(), "stable".into()])
+ .collect();
+ Ok(versions)
+ }
+
+ fn idiomatic_filenames(&self) -> Result> {
+ Ok(vec!["rust-toolchain.toml".into()])
+ }
+
+ fn parse_idiomatic_file(&self, path: &Path) -> Result {
+ let toml = file::read_to_string(path)?;
+ let toml = toml.parse::()?;
+ if let Some(toolchain) = toml.get("toolchain") {
+ if let Some(channel) = toolchain.get("channel") {
+ return Ok(channel.to_string());
+ }
+ }
+ Ok("".into())
+ }
+
+ fn install_version_impl(&self, ctx: &InstallContext, tv: ToolVersion) -> Result {
+ self.setup_rustup(ctx)?;
+
+ CmdLineRunner::new(rustup_bin())
+ .with_pr(ctx.pr.as_ref())
+ .arg("toolchain")
+ .arg("install")
+ .arg(&tv.version)
+ .execute()?;
+
+ self.test_rust(ctx, &tv)?;
+
+ Ok(tv)
+ }
+
+ fn list_bin_paths(&self, _tv: &ToolVersion) -> Result> {
+ Ok(vec![cargo_bindir()])
+ }
+
+ fn exec_env(
+ &self,
+ _config: &Config,
+ _ts: &Toolset,
+ tv: &ToolVersion,
+ ) -> Result> {
+ Ok([("RUSTUP_TOOLCHAIN".to_string(), tv.version.to_string())].into())
+ }
+}
+
+#[cfg(unix)]
+const RUSTC_BIN: &str = "rustc";
+
+#[cfg(windows)]
+const RUSTC_BIN: &str = "rustc.exe";
+
+#[cfg(unix)]
+const RUSTUP_INIT_BIN: &str = "rustup-init";
+
+#[cfg(windows)]
+const RUSTUP_INIT_BIN: &str = "rustup-init.exe";
+
+#[cfg(unix)]
+const RUSTUP_BIN: &str = "rustup";
+
+#[cfg(windows)]
+const RUSTUP_BIN: &str = "rustup.exe";
+
+#[cfg(unix)]
+const CARGO_BIN: &str = "cargo";
+
+#[cfg(windows)]
+const CARGO_BIN: &str = "cargo.exe";
+
+fn rustup_path() -> PathBuf {
+ dirs::CACHE.join("rust").join(RUSTUP_INIT_BIN)
+}
+
+fn rustup_home() -> PathBuf {
+ SETTINGS
+ .rust
+ .rustup_home
+ .clone()
+ .unwrap_or(dirs::HOME.join(".rustup"))
+}
+
+fn rustup_bin() -> PathBuf {
+ cargo_bindir().join(RUSTUP_BIN)
+}
+
+fn cargo_home() -> PathBuf {
+ SETTINGS
+ .rust
+ .cargo_home
+ .clone()
+ .unwrap_or(dirs::HOME.join(".cargo"))
+}
+
+fn cargo_bin() -> PathBuf {
+ cargo_bindir().join(CARGO_BIN)
+}
+fn cargo_bindir() -> PathBuf {
+ cargo_home().join("bin")
+}
+fn rustc_bin() -> PathBuf {
+ cargo_bindir().join(RUSTC_BIN)
+}