Skip to content

Commit eeb7b06

Browse files
committed
Make component removal best-effort and preserve single-error behavior
1 parent 5100b80 commit eeb7b06

File tree

3 files changed

+219
-4
lines changed

3 files changed

+219
-4
lines changed

src/cli/rustup_mode.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,12 +1492,59 @@ async fn component_remove(
14921492
let distributable = DistributableToolchain::from_partial(toolchain, cfg).await?;
14931493
let target = get_target(target, &distributable);
14941494

1495+
let mut parsed_components = Vec::new();
1496+
let mut unknown_components = Vec::new();
1497+
// Preserve the original UnknownComponent error to keep
1498+
// single-component error behavior unchanged
1499+
let mut first_unknown_component_error = None;
1500+
let mut first_error = None;
1501+
14951502
for component in &components {
1496-
let new_component = Component::try_new(component, &distributable, target.as_ref())?;
1497-
distributable.remove_component(new_component).await?;
1503+
match Component::try_new(component, &distributable, target.as_ref()) {
1504+
Ok(new_component) => parsed_components.push((new_component, component)),
1505+
Err(err) => {
1506+
first_error.get_or_insert(err);
1507+
}
1508+
}
14981509
}
14991510

1500-
Ok(ExitCode::SUCCESS)
1511+
for (component, original_name) in parsed_components {
1512+
let Err(err) = distributable.remove_component(component).await else {
1513+
continue;
1514+
};
1515+
1516+
if let Some(RustupError::UnknownComponent { suggestion, .. }) =
1517+
err.downcast_ref::<RustupError>()
1518+
{
1519+
unknown_components.push((original_name.clone(), suggestion.clone()));
1520+
first_unknown_component_error.get_or_insert(err);
1521+
continue;
1522+
}
1523+
1524+
first_error.get_or_insert(err);
1525+
}
1526+
1527+
match unknown_components.len() {
1528+
0 => {}
1529+
1 => {
1530+
return Err(
1531+
first_unknown_component_error.expect("missing first unknown component error")
1532+
);
1533+
}
1534+
_ => {
1535+
return Err(RustupError::UnknownComponents {
1536+
desc: distributable.desc().clone(),
1537+
components: unknown_components,
1538+
}
1539+
.into());
1540+
}
1541+
}
1542+
1543+
if let Some(err) = first_error {
1544+
Err(err)
1545+
} else {
1546+
Ok(ExitCode::SUCCESS)
1547+
}
15011548
}
15021549

15031550
async fn toolchain_link(cfg: &Cfg<'_>, dest: &CustomToolchainName, src: &Path) -> Result<ExitCode> {

src/errors.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::large_enum_variant)]
22

33
use std::ffi::OsString;
4-
use std::fmt::Debug;
4+
use std::fmt::{Debug, Write as FmtWrite};
55
use std::io;
66
use std::io::Write;
77
use std::path::PathBuf;
@@ -134,6 +134,11 @@ pub enum RustupError {
134134
component: String,
135135
suggestion: Option<String>,
136136
},
137+
#[error("{}", unknown_components_msg(.desc, .components))]
138+
UnknownComponents {
139+
desc: ToolchainDesc,
140+
components: Vec<(String, Option<String>)>,
141+
},
137142
#[error(
138143
"toolchain '{desc}' has no prebuilt artifacts available for target '{platform}'\n\
139144
note: this may happen to a low-tier target as per https://doc.rust-lang.org/nightly/rustc/platform-support.html\n\
@@ -242,3 +247,27 @@ fn component_unavailable_msg(cs: &[Component], manifest: &Manifest, toolchain: &
242247

243248
String::from_utf8(buf).unwrap()
244249
}
250+
251+
fn unknown_components_msg(desc: &ToolchainDesc, components: &[(String, Option<String>)]) -> String {
252+
let mut buf = String::new();
253+
254+
match components {
255+
[] => panic!("`unknown_components_msg` should not be called with an empty collection"),
256+
[(component, suggestion)] => {
257+
let _ = write!(
258+
buf,
259+
"toolchain '{desc}' does not contain component '{component}'{}",
260+
suggest_message(suggestion),
261+
);
262+
}
263+
components => {
264+
let _ = writeln!(buf, "toolchain '{desc}' does not contain these components:");
265+
266+
for (component, suggestion) in components {
267+
let _ = writeln!(buf, " - '{component}'{}", suggest_message(suggestion),);
268+
}
269+
}
270+
}
271+
272+
buf
273+
}

tests/suite/cli_rustup.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,145 @@ async fn add_remove_multiple_components() {
21362136
}
21372137
}
21382138

2139+
#[tokio::test]
2140+
async fn remove_multiple_components_is_best_effort_and_order_independent() {
2141+
let files = [
2142+
"lib/rustlib/src/rust-src/foo.rs".to_owned(),
2143+
format!("lib/rustlib/{}/analysis/libfoo.json", this_host_triple()),
2144+
];
2145+
2146+
// Case 1: invalid component first
2147+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
2148+
cx.config
2149+
.expect(&["rustup", "default", "nightly"])
2150+
.await
2151+
.is_ok();
2152+
cx.config
2153+
.expect(&["rustup", "component", "add", "rust-src", "rust-analysis"])
2154+
.await
2155+
.is_ok();
2156+
2157+
for file in &files {
2158+
let path = format!("toolchains/nightly-{}/{}", this_host_triple(), file);
2159+
assert!(cx.config.rustupdir.has(&path));
2160+
}
2161+
2162+
cx.config
2163+
.expect(&[
2164+
"rustup",
2165+
"component",
2166+
"remove",
2167+
"bad-component",
2168+
"rust-src",
2169+
"rust-analysis",
2170+
])
2171+
.await
2172+
.is_err();
2173+
2174+
for file in &files {
2175+
let path = PathBuf::from(format!(
2176+
"toolchains/nightly-{}/{}",
2177+
this_host_triple(),
2178+
file
2179+
));
2180+
assert!(!cx.config.rustupdir.has(path.parent().unwrap()));
2181+
}
2182+
2183+
// Case 2: invalid component last
2184+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
2185+
cx.config
2186+
.expect(&["rustup", "default", "nightly"])
2187+
.await
2188+
.is_ok();
2189+
cx.config
2190+
.expect(&["rustup", "component", "add", "rust-src", "rust-analysis"])
2191+
.await
2192+
.is_ok();
2193+
2194+
for file in &files {
2195+
let path = format!("toolchains/nightly-{}/{}", this_host_triple(), file);
2196+
assert!(cx.config.rustupdir.has(&path));
2197+
}
2198+
2199+
cx.config
2200+
.expect(&[
2201+
"rustup",
2202+
"component",
2203+
"remove",
2204+
"rust-src",
2205+
"rust-analysis",
2206+
"bad-component",
2207+
])
2208+
.await
2209+
.is_err();
2210+
2211+
for file in &files {
2212+
let path = PathBuf::from(format!(
2213+
"toolchains/nightly-{}/{}",
2214+
this_host_triple(),
2215+
file
2216+
));
2217+
assert!(!cx.config.rustupdir.has(path.parent().unwrap()));
2218+
}
2219+
}
2220+
2221+
#[tokio::test]
2222+
async fn remove_multiple_components_reports_all_invalid_names() {
2223+
let files = [
2224+
"lib/rustlib/src/rust-src/foo.rs".to_owned(),
2225+
format!("lib/rustlib/{}/analysis/libfoo.json", this_host_triple()),
2226+
];
2227+
2228+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
2229+
cx.config
2230+
.expect(["rustup", "default", "nightly"])
2231+
.await
2232+
.is_ok();
2233+
2234+
cx.config
2235+
.expect(["rustup", "component", "add", "rust-src", "rust-analysis"])
2236+
.await
2237+
.is_ok();
2238+
2239+
// Ensure components exist
2240+
for file in &files {
2241+
let path = format!("toolchains/nightly-{}/{}", this_host_triple(), file);
2242+
assert!(cx.config.rustupdir.has(&path));
2243+
}
2244+
2245+
cx.config
2246+
.expect([
2247+
"rustup",
2248+
"component",
2249+
"remove",
2250+
"bad-component-1",
2251+
"rust-src",
2252+
"bad-component-2",
2253+
"rust-analysis",
2254+
])
2255+
.await
2256+
.with_stderr(snapbox::str![[r#"
2257+
info: removing component rust-src
2258+
info: removing component rust-analysis
2259+
error: toolchain 'nightly-[HOST_TRIPLE]' does not contain these components:
2260+
- 'bad-component-1'
2261+
- 'bad-component-2'
2262+
2263+
2264+
"#]])
2265+
.is_err();
2266+
2267+
// Ensure valid components were removed
2268+
for file in &files {
2269+
let path = PathBuf::from(format!(
2270+
"toolchains/nightly-{}/{}",
2271+
this_host_triple(),
2272+
file
2273+
));
2274+
assert!(!cx.config.rustupdir.has(path.parent().unwrap()));
2275+
}
2276+
}
2277+
21392278
#[tokio::test]
21402279
async fn file_override() {
21412280
let cx = CliTestContext::new(Scenario::SimpleV2).await;

0 commit comments

Comments
 (0)