Skip to content
Merged
90 changes: 26 additions & 64 deletions src/cli/rustup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use clap_complete::{
};
use futures_util::stream::StreamExt;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use itertools::Itertools;
use serde::Serialize;
use tokio::sync::Semaphore;
use tracing::{info, warn};
Expand Down Expand Up @@ -551,7 +550,7 @@ enum TargetSubcmd {
Remove {
/// List of targets to uninstall
#[arg(required = true, num_args = 1..)]
target: Vec<String>,
target: Vec<TargetTuple>,
Comment thread
djc marked this conversation as resolved.

#[arg(long, help = official_toolchain_arg_help())]
toolchain: Option<PartialToolchainDesc>,
Expand Down Expand Up @@ -1494,7 +1493,7 @@ async fn target_add(
distributable
.add_components(distributable.components()?.into_iter().filter_map(|c| {
(c.available && !c.installed && c.component.short_name() == "rust-std")
.then_some(c.component)
.then_some(Ok(c.component))
}))
.await?;

Expand All @@ -1505,7 +1504,7 @@ async fn target_add(
.add_components(
targets
.into_iter()
.map(|target| Component::std(TargetTuple::new(target))),
.map(|target| Ok(Component::std(TargetTuple::new(target)))),
)
.await?;

Expand All @@ -1514,7 +1513,7 @@ async fn target_add(

async fn target_remove(
cfg: &Cfg<'_>,
targets: Vec<String>,
targets: Vec<TargetTuple>,
toolchain: Option<PartialToolchainDesc>,
) -> anyhow::Result<ExitCode> {
let distributable = DistributableToolchain::from_partial(
Expand All @@ -1523,33 +1522,21 @@ async fn target_remove(
)
.await?;

for target in targets {
let target = TargetTuple::new(target);
let default_target = cfg.default_host_tuple()?;
if target == default_target {
warn!(
"removing the default host target; proc-macros and build scripts might no longer build"
);
}
// Whether we have at most 1 component target that is not `None` (wildcard).
let has_at_most_one_target = distributable
.components()?
.into_iter()
.filter_map(|c| match (c.installed, c.component.target) {
(true, Some(t)) => Some(t),
_ => None,
})
.unique()
.at_most_one()
.is_ok();
if has_at_most_one_target {
warn!("removing the last target; no build targets will be available");
}
distributable
.remove_component(Component::std(target))
.await?;
if targets.contains(&cfg.default_host_tuple()?) {
warn!(
"removing the default host target; proc-macros and build scripts might no longer build"
);
Comment on lines +1525 to +1528

@FranciscoTGouveia FranciscoTGouveia Sep 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Shouldn't we print this after confirming the removal succeeded? I know the phrasing is "removing" and not "removed", but still, a command like rustup target rm <host> non-existent would give the warning and fail without removing anything.

Maybe we could move this to after the actual removal?

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I was looking into the code and did not see that this was already queued. Apologies :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback is still useful!

@rami3l rami3l Sep 18, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Shouldn't we print this after confirming the removal succeeded? I know the phrasing is "removing" and not "removed", but still, a command like rustup target rm <host> non-existent would give the warning and fail without removing anything.

@FranciscoTGouveia Thanks for the feedback!

I'd like to clarify that since #4797, rustup target rm <host> non-existent will cause <host> to be removed, only AFTER which the warning about non-existent will surface (same thing even if you put non-existent before <host>, if you are thinking about that), so we are quite unlikely to encounter false positives here merely due to user input. If the command execution really fails in the middle, I would still like to warn the user at first.

}

let mut remaining_targets = distributable.toolchain.installed_targets()?;
remaining_targets.retain(|it| !targets.contains(it));
if remaining_targets.is_empty() {
warn!("removing the last target; no build targets will be available");
}

distributable
.remove_components(targets.into_iter().map(|c| Ok(Component::std(c))))
.await?;
Ok(ExitCode::SUCCESS)
}

Expand Down Expand Up @@ -1604,9 +1591,7 @@ async fn component_add(
.add_components(
components
.into_iter()
.map(|component| Component::try_new(&component, &distributable, target.as_ref()))
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter(),
.map(|component| Component::try_new(&component, &distributable, target.as_ref())),
)
.await?;

Expand All @@ -1632,37 +1617,14 @@ async fn component_remove(
let distributable = DistributableToolchain::from_partial(toolchain, cfg).await?;
let target = get_target(target, &distributable);

let parsed_components = components
.iter()
.map(|component| Component::try_new(component, &distributable, target.as_ref()))
.collect::<anyhow::Result<Vec<_>>>()?;

let mut unknown_components = Vec::new();

for component in parsed_components {
let Err(err) = distributable.remove_component(component).await else {
continue;
};

if let Some(RustupError::UnknownComponents { components, .. }) =
err.downcast_ref::<RustupError>()
{
unknown_components.extend(components.iter().cloned());
continue;
}

return Err(err);
}

if unknown_components.is_empty() {
Ok(ExitCode::SUCCESS)
} else {
Err(RustupError::UnknownComponents {
desc: distributable.desc().clone(),
components: unknown_components,
}
.into())
}
distributable
.remove_components(
components
.iter()
.map(|component| Component::try_new(component, &distributable, target.as_ref())),
)
.await?;
Ok(ExitCode::SUCCESS)
}

async fn toolchain_link(
Expand Down
6 changes: 6 additions & 0 deletions src/dist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,12 @@ impl TargetTuple {
}
}

impl From<String> for TargetTuple {
fn from(s: String) -> Self {
Self(s)
}
}
Comment thread
rami3l marked this conversation as resolved.

impl fmt::Display for TargetTuple {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
Expand Down
83 changes: 49 additions & 34 deletions src/toolchain/distributable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl<'a> DistributableToolchain<'a> {

pub(crate) async fn add_components(
&self,
components: impl Iterator<Item = Component>,
components: impl IntoIterator<Item = anyhow::Result<Component>>,
) -> anyhow::Result<()> {
let manifestation = self.get_manifestation()?;
let manifest = self.get_manifest()?;
Expand All @@ -79,9 +79,11 @@ impl<'a> DistributableToolchain<'a> {
.get(&self.desc.target)
.expect("installed manifest should have a known target");

let components = components.into_iter();
let mut validated_components = Vec::with_capacity(components.size_hint().0);

for mut component in components {
for component in components {
let mut component = component?;
if let Some(c) = manifest.rename_component(&component) {
component = c;
}
Expand Down Expand Up @@ -383,59 +385,72 @@ impl<'a> DistributableToolchain<'a> {
}
}

pub(crate) async fn remove_component(&self, mut component: Component) -> anyhow::Result<()> {
// TODO: take multiple components?
pub(crate) async fn remove_components(
&self,
components: impl IntoIterator<Item = anyhow::Result<Component>>,
) -> anyhow::Result<()> {
let manifestation = self.get_manifestation()?;
let config = manifestation.read_config()?.unwrap_or_default();
let manifest = self.get_manifest()?;

// Rename the component if necessary.
if let Some(c) = manifest.rename_component(&component) {
component = c;
}
let components = components.into_iter();
let mut renamed_components = Vec::with_capacity(components.size_hint().0);
let mut unknown_components = vec![];
for component in components {
let mut component = component?;
if let Some(renamed) = manifest.rename_component(&component) {
component = renamed;
}
if config.components.contains(&component) {
renamed_components.push(component);
continue;
}

if !config.components.contains(&component) {
let wildcard_component = component.wildcard();
if config.components.contains(&wildcard_component) {
component = wildcard_component;
} else {
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())
{
return Err(RustupError::TargetNotInstalled {
desc: Box::new(self.desc.clone()),
target: component.target.expect("component target should be known"),
suggestion,
}
.into());
}
return Err(RustupError::UnknownComponents {
desc: self.desc.clone(),
components: vec![UnknownComponentInfo {
name: manifest.short_name(&component).to_string(),
description: manifest.description(&component),
suggestion,
}],
renamed_components.push(wildcard_component);
continue;
}

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())
{
return Err(RustupError::TargetNotInstalled {
desc: Box::new(self.desc.clone()),
target: component.target.expect("component target should be known"),
suggestion,
}
.into());
}
unknown_components.push(UnknownComponentInfo {
name: manifest.short_name(&component).to_string(),
description: manifest.description(&component),
suggestion,
});
}

let changes = Changes {
explicit_add_components: vec![],
remove_components: vec![component],
remove_components: renamed_components,
};

let download_cfg = DownloadCfg::new(self.toolchain.cfg);
manifestation
.update(manifest, changes, false, &download_cfg, &self.desc, false)
.await?;

if !unknown_components.is_empty() {
return Err(RustupError::UnknownComponents {
desc: self.desc().clone(),
components: unknown_components,
}
.into());
}

Ok(())
}

Expand Down
Loading