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