diff --git a/src/toolchain.rs b/src/toolchain.rs index f13ef0aaf3..2791a415da 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -284,6 +284,7 @@ impl<'a> Toolchain<'a> { #[tracing::instrument(level = "trace")] pub fn rustc_version(&self) -> String { match self.create_command("rustc") { + Err(e) => format!("(rustc does not exist: {e})"), Ok(mut cmd) => { cmd.arg("--version"); cmd.stdin(Stdio::null()); @@ -291,40 +292,41 @@ impl<'a> Toolchain<'a> { cmd.stderr(Stdio::piped()); self.set_ldpath(&mut cmd); + let mut child = match cmd.spawn() { + Err(e) => return format!("(error reading rustc version: {e})"), + Ok(child) => child, + }; // some toolchains are faulty with some combinations of platforms and // may fail to launch but also to timely terminate. // (known cases include Rust 1.3.0 through 1.10.0 in recent macOS Sierra.) // we guard against such cases by enforcing a reasonable timeout to read. - let mut line1 = None; - if let Ok(mut child) = cmd.spawn() { - let timeout = Duration::new(10, 0); - match child.wait_timeout(timeout) { - Ok(Some(status)) if status.success() => { - let out = child - .stdout - .expect("Child::stdout requested but not present"); - let mut line = String::new(); - if BufReader::new(out).read_line(&mut line).is_ok() { - let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); - line.truncate(lineend); - line1 = Some(line); - } - } - Ok(None) => { - let _ = child.kill(); - return String::from("(timeout reading rustc version)"); - } - Ok(Some(_)) | Err(_) => {} + let timeout = Duration::new(10, 0); + let status = match child.wait_timeout(timeout) { + Ok(None) => { + let _ = child.kill(); + return String::from("(timeout reading rustc version)"); } + Err(e) => return format!("(error reading rustc version: {e})"), + Ok(Some(status)) => status, + }; + + if !status.success() { + return format!("(error reading rustc version: {status})"); } - if let Some(line1) = line1 { - line1 - } else { - String::from("(error reading rustc version)") + let out = child + .stdout + .expect("Child::stdout requested but not present"); + let mut line = String::new(); + match BufReader::new(out).read_line(&mut line) { + Ok(_) => { + let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); + line.truncate(lineend); + line + } + Err(e) => format!("(error reading rustc version: {e})"), } } - Err(_) => String::from("(rustc does not exist)"), } } diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 229c5650b3..c3b50d8e99 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -1391,6 +1391,64 @@ nightly-[HOST_TRIPLE] (default) .is_ok(); } +#[tokio::test] +async fn show_active_toolchain_rustc_missing() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + + let path = cx.config.customdir.join("custom-1"); + let path_str = path.to_string_lossy(); + cx.config + .expect(&["rustup", "toolchain", "link", "custom", &path_str]) + .await + .is_ok(); + cx.config + .expect(&["rustup", "default", "custom"]) + .await + .is_ok(); + fs::remove_file(path.join("bin/rustc")).unwrap(); + cx.config + .expect(&["rustup", "show", "active-toolchain", "--verbose"]) + .await + .with_stdout(snapbox::str![ + r#" +... +compiler: (rustc does not exist: [..]) +... + "# + ]) + .is_err(); +} + +#[cfg(unix)] +#[tokio::test] +async fn show_active_toolchain_rustc_version_error() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let path = cx.config.customdir.join("custom-1"); + let path_str = path.to_string_lossy(); + cx.config + .expect(["rustup", "toolchain", "link", "custom", &path_str]) + .await + .is_ok(); + cx.config + .expect(["rustup", "default", "custom"]) + .await + .is_ok(); + // Replace mock rustc with a shell script that exits 1 + let rustc_path = path.join(format!("bin/rustc{EXE_SUFFIX}")); + fs::write(&rustc_path, "#!/bin/sh\nexit 1\n").unwrap(); + cx.config + .expect(["rustup", "show", "active-toolchain", "--verbose"]) + .await + .extend_redactions([("[RUSTUP_DIR]", &cx.config.rustupdir.to_string())]) + .with_stdout(snapbox::str![[r#" +... +compiler: (error reading rustc version: exit status: [..]) +... +"#]]) + .with_stderr(snapbox::str![[""]]) + .is_ok(); +} + #[tokio::test] async fn show_with_verbose() { let mut cx = CliTestContext::new(Scenario::None).await; @@ -1441,6 +1499,42 @@ installed targets: .is_ok(); } +#[cfg(unix)] +#[tokio::test] +async fn show_active_toolchain_rustc_permission_denied() { + use std::os::unix::fs::PermissionsExt; + + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let path = cx.config.customdir.join("custom-1"); + let path_str = path.to_string_lossy(); + cx.config + .expect(["rustup", "toolchain", "link", "custom", &path_str]) + .await + .is_ok(); + cx.config + .expect(["rustup", "default", "custom"]) + .await + .is_ok(); + // Remove the executable bit so that spawn() fails with a permission error. + let rustc_path = path.join(format!("bin/rustc{EXE_SUFFIX}")); + let mut perms = fs::metadata(&rustc_path).unwrap().permissions(); + perms.set_mode(0o644); + fs::set_permissions(&rustc_path, perms).unwrap(); + cx.config + .expect(["rustup", "show", "active-toolchain", "--verbose"]) + .await + .extend_redactions([("[RUSTUP_DIR]", &cx.config.rustupdir.to_string())]) + .with_stdout(snapbox::str![[r#" +custom +active because: it's the default toolchain +compiler: (error reading rustc version: Permission denied [..]) +path: [RUSTUP_DIR]/toolchains/custom + +"#]]) + .with_stderr(snapbox::str![[""]]) + .is_ok(); +} + #[tokio::test] async fn show_active_toolchain_with_verbose() { let cx = CliTestContext::new(Scenario::SimpleV2).await;