diff --git a/Cargo.lock b/Cargo.lock index 391c6e8655..7bbdb96ef4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,6 +514,12 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dunce" version = "1.0.5" @@ -2053,6 +2059,7 @@ dependencies = [ "clap-cargo", "clap_complete", "console", + "dotenvy", "effective-limits", "enum-map", "env_proxy", diff --git a/Cargo.toml b/Cargo.toml index 2e4af2652f..4d69f7ef86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ clap = { version = "4", features = ["derive", "wrap_help", "string"] } clap-cargo = "0.19.0" clap_complete = { version = "4", features = ["unstable-dynamic"] } console = "0.16" +dotenvy = "0.15.7" effective-limits = "0.5.5" enum-map = "3.0.0" env_proxy = { version = "0.4.1", optional = true } diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index 7e836b034a..67aa095d7a 100644 --- a/doc/user-guide/src/environment-variables.md +++ b/doc/user-guide/src/environment-variables.md @@ -1,5 +1,13 @@ # Environment variables +Rustup loads environment variables from an optional `${RUSTUP_HOME}/.env`. +For example: + +```dotenv +RUSTUP_DIST_SERVER=https://example.com/rust-static +RUSTUP_TOOLCHAIN=stable +``` + - `RUSTUP_LOG` (default: none). Enables Rustup's "custom logging mode". In this mode, the verbosity of Rustup's log lines can be specified with `tracing_subscriber`'s [directive syntax]. For example, set `RUSTUP_LOG=rustup=DEBUG` to receive log lines diff --git a/src/bin/rustup-init.rs b/src/bin/rustup-init.rs index ded74b236a..031865c235 100644 --- a/src/bin/rustup-init.rs +++ b/src/bin/rustup-init.rs @@ -37,6 +37,7 @@ fn main() -> Result { pre_rustup_main_init(); let process = Process::os(); + process.load_dotenv()?; let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(process.io_thread_count()?.into()) diff --git a/src/process.rs b/src/process.rs index 0582544e1d..16bdda1583 100644 --- a/src/process.rs +++ b/src/process.rs @@ -49,6 +49,16 @@ impl Process { Self::OsProcess(OsProcess::new()) } + pub fn load_dotenv(&self) -> Result<()> { + let path = self.rustup_home()?.join(".env"); + match dotenvy::from_path(&path) { + Ok(()) => Ok(()), + Err(error) if error.not_found() => Ok(()), + Err(error) => Err(error) + .with_context(|| format!("failed to load environment file '{}'", path.display())), + } + } + pub fn name(&self) -> Option { let arg0 = match self.var("RUSTUP_FORCE_ARG0") { Ok(v) => Some(v), diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 2894b50fe2..650dbf5f1c 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -4,14 +4,15 @@ use std::{ env::consts::EXE_SUFFIX, fs, path::{Path, PathBuf}, + process::Command, }; use rustup::{ env_var::RUST_RECURSION_COUNT_MAX, for_host, test::{ - CROSS_ARCH1, CROSS_ARCH2, CliTestContext, MULTI_ARCH1, Scenario, this_host_tuple, - topical_doc_data, + Assert, CROSS_ARCH1, CROSS_ARCH2, CliTestContext, MULTI_ARCH1, SanitizedOutput, Scenario, + this_host_tuple, topical_doc_data, }, utils::raw, }; @@ -1416,6 +1417,64 @@ installed targets: "#]]); } +fn run_active_toolchain_subprocess(mut command: Command) -> Assert { + let output = command.output().unwrap(); + Assert::new(SanitizedOutput { + status: output.status.code(), + stdout: String::from_utf8(output.stdout).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + }) +} + +#[tokio::test] +async fn show_toolchain_env_from_dotenv() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect(["rustup", "toolchain", "install", "nightly", "beta"]) + .await + .is_ok(); + fs::write( + cx.config.rustupdir.join(".env"), + "RUSTUP_TOOLCHAIN=nightly\n", + ) + .unwrap(); + + let mut command = cx.config.cmd("rustup", ["show", "active-toolchain"]); + command.env_remove("RUSTUP_TOOLCHAIN"); + run_active_toolchain_subprocess(command) + .is_ok() + .with_stdout(snapbox::str![[r#" +nightly-[HOST_TUPLE] (overridden by environment variable RUSTUP_TOOLCHAIN) + +"#]]) + .with_stderr(snapbox::str![[""]]); + + let mut command = cx.config.cmd("rustup", ["show", "active-toolchain"]); + command.env("RUSTUP_TOOLCHAIN", "beta"); + run_active_toolchain_subprocess(command) + .is_ok() + .with_stdout(snapbox::str![[r#" +beta-[HOST_TUPLE] (overridden by environment variable RUSTUP_TOOLCHAIN) + +"#]]) + .with_stderr(snapbox::str![[""]]); +} + +#[tokio::test] +async fn invalid_dotenv_is_reported() { + let cx = CliTestContext::new(Scenario::None).await; + fs::write(cx.config.rustupdir.join(".env"), "invalid line\n").unwrap(); + + let mut command = cx.config.cmd("rustup", ["--version"]); + command.env_remove("RUSTUP_TOOLCHAIN"); + let output = command.output().unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!(!output.status.success()); + assert!(stderr.contains("failed to load environment file")); + assert!(stderr.contains(".env")); +} + #[tokio::test] async fn show_toolchain_env_not_installed() { let cx = CliTestContext::new(Scenario::SimpleV2).await;