diff --git a/Cargo.lock b/Cargo.lock
index 2137369302..433b510f3f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1774,6 +1774,16 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "junction"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16"
+dependencies = [
+ "scopeguard",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "kdl"
version = "4.6.0"
@@ -2147,6 +2157,7 @@ dependencies = [
"indoc",
"insta",
"itertools",
+ "junction",
"log",
"md-5",
"nix",
diff --git a/Cargo.toml b/Cargo.toml
index 81a96b1605..63a1cdf0ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -81,6 +81,7 @@ indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.17", features = ["default", "improved_unicode"] }
indoc = "2"
itertools = "0.13"
+junction = "1"
log = "0.4"
md-5 = "0.10"
num_cpus = "1"
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-win/rust.Tests.ps1 b/e2e-win/rust.Tests.ps1
new file mode 100644
index 0000000000..4a900f874a
--- /dev/null
+++ b/e2e-win/rust.Tests.ps1
@@ -0,0 +1,6 @@
+
+Describe 'node' {
+ It 'executes rust 1.82.0' {
+ mise x rust@1.82.0 -- rustc -V | Should -BeLike "rustc 1.82.0*"
+ }
+}
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/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..28c92cd000 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -309,8 +309,10 @@ pub fn make_symlink(target: &Path, link: &Path) -> Result<(PathBuf, PathBuf)> {
#[cfg(windows)]
//#[deprecated]
-pub fn make_symlink(_target: &Path, _link: &Path) -> Result<(PathBuf, PathBuf)> {
- unimplemented!("make_symlink is not implemented on Windows")
+pub fn make_symlink(target: &Path, link: &Path) -> Result<(PathBuf, PathBuf)> {
+ junction::create(target, link)
+ .wrap_err_with(|| format!("failed to ln -sf {} {}", target.display(), link.display()))?;
+ Ok((target.to_path_buf(), link.to_path_buf()))
}
#[cfg(windows)]
@@ -403,8 +405,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..ba599b04c4
--- /dev/null
+++ b/src/plugins/core/rust.rs
@@ -0,0 +1,185 @@
+use std::collections::BTreeMap;
+use std::path::{Path, PathBuf};
+
+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};
+use eyre::Result;
+
+#[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, tv: &ToolVersion) -> 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")
+ .envs(self.exec_env(&CONFIG, CONFIG.get_toolset()?, tv)?);
+ 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)?)
+ .prepend_path(self.list_bin_paths(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, &tv)?;
+
+ CmdLineRunner::new(RUSTUP_BIN)
+ .with_pr(ctx.pr.as_ref())
+ .arg("toolchain")
+ .arg("install")
+ .arg(&tv.version)
+ .prepend_path(self.list_bin_paths(&tv)?)?
+ .envs(self.exec_env(&CONFIG, CONFIG.get_toolset()?, &tv)?)
+ .execute()?;
+
+ file::remove_all(tv.install_path())?;
+ file::make_symlink(&cargo_home().join(&tv.version), &tv.install_path())?;
+
+ 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> {
+ let toolchain = tv.version.to_string();
+ Ok([
+ (
+ "CARGO_HOME".to_string(),
+ cargo_home().to_string_lossy().to_string(),
+ ),
+ (
+ "RUSTUP_HOME".to_string(),
+ rustup_home().to_string_lossy().to_string(),
+ ),
+ ("RUSTUP_TOOLCHAIN".to_string(), toolchain),
+ ]
+ .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 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")
+}