Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ use crate::{

pub(crate) const DEFAULT_STABLE_HINT: &str = "help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain.";

#[derive(Debug, Clone)]
pub enum TargetSuggestion {
Toolchain(String),
Component(String),
}

impl std::fmt::Display for TargetSuggestion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Toolchain(command) => write!(f, "; try `{command}`"),
Self::Component(name) => write!(f, "; did you mean '{name}'?"),
}
}
}

/// A type erasing thunk for the retry crate to permit use with anyhow. See <https://github.com/dtolnay/anyhow/issues/149>
#[derive(Debug, ThisError)]
#[error(transparent)]
Expand Down Expand Up @@ -162,11 +177,11 @@ pub enum RustupError {
suggestion: Option<String>,
},
#[error("toolchain '{}' does not have target '{}' installed{}\n", .desc, .target,
suggest_message(.suggestion))]
.suggestion.as_ref().map_or_else(String::new, ToString::to_string))]
TargetNotInstalled {
desc: Box<ToolchainDesc>,
target: TargetTuple,
suggestion: Option<String>,
suggestion: Option<TargetSuggestion>,
},
#[error(
"rustup executable proxies don't seem to work\n\
Expand Down
41 changes: 37 additions & 4 deletions src/toolchain/distributable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ use crate::{
RustupError, component_for_bin,
config::{ActiveSource, Cfg, EnsureInstalled},
dist::{
DistOptions, PartialToolchainDesc, ToolchainDesc,
DistOptions, PartialToolchainDesc, TargetTuple, ToolchainDesc,
config::Config,
download::DownloadCfg,
manifest::{Component, ComponentStatus, Manifest, ManifestWithHash},
manifestation::{Changes, Manifestation},
prefix::InstallPrefix,
},
errors::UnknownComponentInfo,
errors::{TargetSuggestion, UnknownComponentInfo},
install::InstallMethod,
};

Expand Down Expand Up @@ -385,6 +385,27 @@ impl<'a> DistributableToolchain<'a> {
}
}

fn find_toolchain_with_target(&self, target: &TargetTuple) -> Option<String> {
let toolchains = self.toolchain.cfg.list_toolchains(true).ok()?;

for toolchain_name in toolchains {
if let ToolchainName::Official(desc) = &toolchain_name
&& desc == &self.desc
{
continue;
}

if let Ok(toolchain) = Toolchain::new(self.toolchain.cfg, toolchain_name.clone().into())
&& let Ok(installed_targets) = toolchain.installed_targets()
&& installed_targets.contains(target)
{
return Some(format!("+{toolchain_name}"));
}
}

None
}

pub(crate) async fn remove_components(
&self,
components: impl IntoIterator<Item = anyhow::Result<Component>>,
Expand Down Expand Up @@ -413,19 +434,31 @@ impl<'a> DistributableToolchain<'a> {
}

let suggestion = self.get_component_suggestion(&component, &config, &manifest, true);
// Check if the target is installed.
if !config
.components
.iter()
.any(|c| c.target() == component.target())
{
let target = component
.target
.as_ref()
.expect("component target should be known");
let suggestion = self
.find_toolchain_with_target(target)
.map(|toolchain| {
TargetSuggestion::Toolchain(format!(
"rustup {toolchain} target remove {target}"
))
})
.or_else(|| suggestion.map(TargetSuggestion::Component));
return Err(RustupError::TargetNotInstalled {
desc: Box::new(self.desc.clone()),
target: component.target.expect("component target should be known"),
target: target.clone(),
suggestion,
}
.into());
}

unknown_components.push(UnknownComponentInfo {
name: manifest.short_name(&component).to_string(),
description: manifest.description(&component),
Expand Down
66 changes: 66 additions & 0 deletions tests/suite/cli_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,72 @@ error: toolchain 'nightly-[HOST_TUPLE]' does not have target '[CROSS_ARCH_I]' in
.is_err();
}

#[tokio::test]
async fn remove_target_not_installed_with_suggestion() {
let cx = CliTestContext::new(Scenario::SimpleV2).await;
cx.config
.expect(["rustup", "toolchain", "install", "stable"])
.await
.is_ok();
cx.config
.expect(["rustup", "toolchain", "install", "nightly"])
.await
.is_ok();
cx.config
.expect(["rustup", "default", "stable"])
.await
.is_ok();
cx.config
.expect([
"rustup",
"target",
"add",
CROSS_ARCH1,
"--toolchain=nightly",
])
.await
.is_ok();
cx.config
.expect(["rustup", "target", "remove", CROSS_ARCH1])
.await
.extend_redactions([
("[HOST_TUPLE]", this_host_tuple()),
("[CROSS_ARCH_I]", CROSS_ARCH1.to_string()),
])
.with_stderr(snapbox::str![[r#"
...
error: toolchain 'stable-[HOST_TUPLE]' does not have target '[CROSS_ARCH_I]' installed; try `rustup +nightly-[HOST_TUPLE] target remove [CROSS_ARCH_I]`
...
"#]])
.is_err();
}

#[tokio::test]
async fn remove_target_not_installed_no_alternative() {
let cx = CliTestContext::new(Scenario::SimpleV2).await;
cx.config
.expect(["rustup", "toolchain", "install", "stable"])
.await
.is_ok();
cx.config
.expect(["rustup", "default", "stable"])
.await
.is_ok();
cx.config
.expect(["rustup", "target", "remove", CROSS_ARCH1])
.await
.extend_redactions([
("[HOST_TUPLE]", this_host_tuple()),
("[CROSS_ARCH_I]", CROSS_ARCH1.to_string()),
])
.with_stderr(snapbox::str![[r#"
...
error: toolchain 'stable-[HOST_TUPLE]' does not have target '[CROSS_ARCH_I]' installed
...
"#]])
.is_err();
}

#[tokio::test]
async fn remove_target_no_toolchain() {
let cx = CliTestContext::new(Scenario::SimpleV2).await;
Expand Down
Loading