diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 532867c50c..a19fb3bf69 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -190,6 +190,10 @@ enum RustupSubcmd { #[arg(long)] no_self_update: bool, + /// Allow downgrading the toolchain + #[arg(long)] + allow_downgrade: bool, + /// Force an update, even if some components are missing #[arg(long)] force: bool, @@ -750,6 +754,7 @@ pub async fn main( RustupSubcmd::Update { toolchain, no_self_update, + allow_downgrade, force, force_non_host, } => { @@ -758,6 +763,7 @@ pub async fn main( UpdateOpts { toolchain, no_self_update, + allow_downgrade, force, force_non_host, ..UpdateOpts::default() diff --git a/src/dist/manifestation/tests.rs b/src/dist/manifestation/tests.rs index db95e4d2bb..15ee2bcc28 100644 --- a/src/dist/manifestation/tests.rs +++ b/src/dist/manifestation/tests.rs @@ -17,7 +17,7 @@ use url::Url; use crate::{ config::Cfg, dist::{ - DEFAULT_DIST_SERVER, Profile, TargetTuple, ToolchainDesc, + DEFAULT_DIST_SERVER, DistOptions, Profile, TargetTuple, ToolchainDesc, download::{DownloadCfg, DownloadTracker}, manifest::{Component, Manifest}, manifestation::{Changes, Manifestation, UpdateStatus}, @@ -1573,25 +1573,16 @@ async fn v2_manifest_checksum_mismatch_surfaces_error() { let tp = TestProcess::new(env::current_dir().unwrap(), &["rustup"], vars, ""); let cfg = Cfg::from_env(tp.process.current_dir().unwrap(), false, true, &tp.process).unwrap(); - let dl_cfg = DownloadCfg::new(&cfg); - let update_hash = cfg.get_hash_file(&cx.toolchain, true).unwrap(); - let mut fetched = String::new(); - - let err = super::super::try_update_from_dist_( - &dl_cfg, - &update_hash, - &cx.toolchain, - Some(Profile::Default), - &cx.prefix, - false, - &[], - &[], - &mut fetched, - &cfg, - None, - ) - .await - .unwrap_err(); + + let dist_opts = + DistOptions::new(&[], &[], &cx.toolchain, Profile::Default, false, &cfg).unwrap(); + let manifest_result = dist_opts + .dl_v2_manifest(&cx.prefix, dist_opts.toolchain) + .await; + let err = dist_opts + .try_update(None, &cx.prefix, manifest_result) + .await + .unwrap_err(); match err.downcast_ref::() { Some(RustupError::ChecksumFailed { .. }) => {} diff --git a/src/dist/mod.rs b/src/dist/mod.rs index f348188943..9e3c9075f2 100644 --- a/src/dist/mod.rs +++ b/src/dist/mod.rs @@ -1,16 +1,11 @@ //! Installation from a Rust distribution server use std::{ - collections::HashSet, - env, fmt, - io::Write, - ops::Deref, - path::{Path, PathBuf}, - str::FromStr, + collections::HashSet, env, fmt, io::Write, ops::Deref, path::PathBuf, str::FromStr, sync::LazyLock, }; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow}; use chrono::NaiveDate; use clap::{ValueEnum, builder::PossibleValue}; use itertools::Itertools; @@ -944,7 +939,7 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { pub(crate) async fn install_into( &self, prefix: &InstallPrefix, - manifest: Option, + mut prefetched_manifest: Option, ) -> Result> { let fresh_install = !prefix.path().exists(); // fresh_install means the toolchain isn't present, but hash_exists means there is a stray hash file @@ -956,7 +951,6 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { std::fs::remove_file(&self.update_hash)?; } - let mut fetched = String::new(); let mut first_err = None; let backtrack = self.toolchain.channel == Channel::Nightly && self.toolchain.date.is_none(); // We want to limit backtracking if we do not already have a toolchain @@ -1003,26 +997,41 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { }; let mut toolchain = self.toolchain.clone(); - let mut prefetched_manifest = manifest; let res = loop { - let result = try_update_from_dist_( - &self.dl_cfg, - &self.update_hash, - &toolchain, - match self.exists { - false => Some(self.profile), - true => None, - }, - prefix, - self.force, - self.components, - self.targets, - &mut fetched, - self.cfg, - prefetched_manifest.take(), - ) - .await; + // TODO: Add a notification about which manifest version is going to be used + info!("syncing channel updates for {toolchain}"); + let manifest_result = match prefetched_manifest.take() { + Some(manifest) => Ok(Some(manifest)), + None => self.dl_v2_manifest(prefix, &toolchain).await, + }; + + // Skip installation when the toolchain to be installed comes with a valid manifest date + // that is older than the oldest acceptable date. + let try_date_str = if let Some(date) = &toolchain.date { + Some(date) + } else if let Ok(Some(ManifestWithHash { manifest, .. })) = &manifest_result { + Some(&manifest.date) + } else { + None + }; + let try_date = try_date_str + .and_then(|s| date_from_manifest_date(s)) + // Parsing error can only be a problem if we enter backtracking, so we delay the + // error until we actually need to try another date. + .ok_or_else(|| format!("malformed manifest date: {try_date_str:?}")); + if try_date.as_ref().is_ok_and(|date| date < &last_manifest) { + break match first_err { + // Wouldn't be an update if we go further back than the currently installed toolchain. + Some(e) => Err(e), + // In this case, all newer nightlies are missing, which means there are no + // updates, so the user is already at the latest nightly. + None => Ok(None), + }; + } + let result = self + .try_update(Some(&toolchain), prefix, manifest_result) + .await; let e = match result { Ok(v) => break Ok(v), Err(e) if !backtrack => break Err(e), @@ -1076,23 +1085,10 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { // the components that the user currently has installed. Let's try the previous // nightlies in reverse chronological order until we find a nightly that does, // starting at one date earlier than the current manifest's date. - let toolchain_date = toolchain.date.as_ref().unwrap_or(&fetched); - let try_next = date_from_manifest_date(toolchain_date) - .unwrap_or_else(|| panic!("Malformed manifest date: {toolchain_date:?}")) + let try_next = try_date + .unwrap_or_else(|e| panic!("{e}")) .pred_opt() .unwrap(); - - if try_next < last_manifest { - // Wouldn't be an update if we go further back than the user's current nightly. - if let Some(e) = first_err { - break Err(e); - } else { - // In this case, all newer nightlies are missing, which means there are no - // updates, so the user is already at the latest nightly. - break Ok(None); - } - } - toolchain.date = Some(try_next.format("%Y-%m-%d").to_string()); }; @@ -1104,177 +1100,170 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { res } -} -#[allow(clippy::too_many_arguments)] -async fn try_update_from_dist_( - download: &DownloadCfg<'_>, - update_hash: &Path, - toolchain: &ToolchainDesc, - profile: Option, - prefix: &InstallPrefix, - force_update: bool, - components: &[&str], - targets: &[&str], - fetched: &mut String, - cfg: &Cfg<'_>, - prefetched_manifest: Option, -) -> Result> { - let toolchain_str = toolchain.to_string(); - let manifestation = Manifestation::open(prefix.clone(), toolchain.target.clone())?; - - // TODO: Add a notification about which manifest version is going to be used - info!("syncing channel updates for {toolchain_str}"); - let manifest_result = if prefetched_manifest.is_some() { - Ok(prefetched_manifest) - } else { - download - .dl_v2_manifest( - // Skip the update hash when the installed manifest is missing or when components - // or targets were requested, since either case still requires the channel manifest. - if prefix.dist_manifest().is_some() && components.is_empty() && targets.is_empty() { - Some(update_hash) - } else { - None - }, - toolchain, - cfg, - ) - .await - }; - match manifest_result { - Ok(Some(ManifestWithHash { manifest: m, hash })) => { - match m.get_rust_version() { - Ok(version) => info!("latest update on {} for version {version}", m.date), - Err(_) => info!("latest update on {}", m.date), - } - - let profile_components = match profile { - Some(profile) => m.get_profile_components(profile, &toolchain.target)?, - None => Vec::new(), - }; - - let mut all_components: HashSet = profile_components.into_iter().collect(); - - let rust_package = m.get_package("rust")?; - let rust_target_package = rust_package.get_target(Some(&toolchain.target.clone()))?; - - for component in components { - let mut component = - Component::new(component.to_string(), Some(toolchain.target.clone()), false); - if let Some(renamed) = m.rename_component(&component) { - component = renamed; - } - // Look up the newly constructed/renamed component and ensure that - // if it's a wildcard component we note such, otherwise we end up - // exacerbating the problem we thought we'd fixed with #2087 and #2115 - if let Some(c) = rust_target_package - .components - .iter() - .find(|c| c.short_name() == component.short_name()) - && c.target.is_none() - { - component = component.wildcard(); + pub(crate) async fn try_update( + &self, + toolchain: Option<&ToolchainDesc>, + prefix: &InstallPrefix, + manifest_result: Result>, + ) -> Result> { + let download = &self.dl_cfg; + let toolchain = toolchain.unwrap_or(self.toolchain); + let manifestation = Manifestation::open(prefix.clone(), toolchain.target.clone())?; + + match manifest_result { + Ok(Some(ManifestWithHash { manifest: m, hash })) => { + match m.get_rust_version() { + Ok(version) => info!("latest update on {} for version {version}", m.date), + Err(_) => info!("latest update on {}", m.date), } - all_components.insert(component); - } - for &target in targets { - let tuple = TargetTuple::new(target); - all_components.insert(Component::new("rust-std".to_string(), Some(tuple), false)); - } + let profile_components = match self.exists { + true => Vec::new(), + false => m.get_profile_components(self.profile, &toolchain.target)?, + }; - let mut explicit_add_components: Vec<_> = all_components.into_iter().collect(); - explicit_add_components.sort(); + let mut all_components: HashSet = + profile_components.into_iter().collect(); + + let rust_package = m.get_package("rust")?; + let rust_target_package = + rust_package.get_target(Some(&toolchain.target.clone()))?; + + for &component in self.components { + let mut component = Component::new( + component.to_string(), + Some(toolchain.target.clone()), + false, + ); + if let Some(renamed) = m.rename_component(&component) { + component = renamed; + } + // Look up the newly constructed/renamed component and ensure that + // if it's a wildcard component we note such, otherwise we end up + // exacerbating the problem we thought we'd fixed with #2087 and #2115 + if let Some(c) = rust_target_package + .components + .iter() + .find(|c| c.short_name() == component.short_name()) + && c.target.is_none() + { + component = component.wildcard(); + } + all_components.insert(component); + } - let changes = Changes { - explicit_add_components, - remove_components: Vec::new(), - }; + for &target in self.targets { + let tuple = TargetTuple::new(target); + all_components.insert(Component::new( + "rust-std".to_string(), + Some(tuple), + false, + )); + } - fetched.clone_from(&m.date); + let mut explicit_add_components: Vec<_> = all_components.into_iter().collect(); + explicit_add_components.sort(); - return match manifestation - .update(m, changes, force_update, download, toolchain, true) - .await - { - Ok(status) => match status { - UpdateStatus::Unchanged => Ok(None), - UpdateStatus::Changed => Ok(Some(hash)), - }, - // Check for the variant by reference before we downcast with ownership, - // otherwise we'll drop implicit context bundled up in the original anyhow::Error. - Err(err) => match err.downcast_ref::() { - Some(RustupError::RequestedComponentsUnavailable { .. }) => { - let Ok(RustupError::RequestedComponentsUnavailable { - components, - manifest, - toolchain, - }) = err.downcast::() - else { - unreachable!() - }; - - Err(anyhow!(DistError::ToolchainComponentsMissing( - components, manifest, toolchain, - ))) + let changes = Changes { + explicit_add_components, + remove_components: Vec::new(), + }; + return match manifestation + .update(m, changes, self.force, download, toolchain, true) + .await + { + Ok(status) => match status { + UpdateStatus::Unchanged => Ok(None), + UpdateStatus::Changed => Ok(Some(hash)), + }, + // Check for the variant by reference before we downcast with ownership, + // otherwise we'll drop implicit context bundled up in the original anyhow::Error. + Err(err) => match err.downcast_ref::() { + Some(RustupError::RequestedComponentsUnavailable { .. }) => { + let Ok(RustupError::RequestedComponentsUnavailable { + components, + manifest, + toolchain, + }) = err.downcast::() + else { + unreachable!() + }; + + Err(anyhow!(DistError::ToolchainComponentsMissing( + components, manifest, toolchain, + ))) + } + Some(_) | None => Err(err), + }, + }; + } + Ok(None) => return Ok(None), + Err(err) => { + match err.downcast_ref::() { + Some(RustupError::DownloadNotExists { .. }) => { + // Proceed to try v1 as a fallback + debug!("manifest not found; trying legacy manifest"); } - Some(_) | None => Err(err), - }, - }; - } - Ok(None) => return Ok(None), - Err(err) => { - match err.downcast_ref::() { - Some(RustupError::DownloadNotExists { .. }) => { - // Proceed to try v1 as a fallback - debug!("manifest not found; trying legacy manifest"); + // Includes `ChecksumFailed`: if the v2 manifest exists but its + // contents do not match the published `.sha256`, surface the + // integrity failure as an error rather than silently treating + // the toolchain as up to date. The v1 fallback path below + // already does the same. + _ => return Err(err), } - // Includes `ChecksumFailed`: if the v2 manifest exists but its - // contents do not match the published `.sha256`, surface the - // integrity failure as an error rather than silently treating - // the toolchain as up to date. The v1 fallback path below - // already does the same. - _ => return Err(err), } } - } - // If the v2 manifest is not found then try v1 - let manifest = match download.dl_v1_manifest(&cfg.dist_root_url, toolchain).await { - Ok(m) => m, - Err(err) => match err.downcast_ref::() { - Some(RustupError::ChecksumFailed { .. }) => return Err(err), - Some(RustupError::DownloadNotExists { .. }) => { - bail!(DistError::MissingReleaseForToolchain( + // If the v2 manifest is not found then try v1 + let manifest = download + .dl_v1_manifest(&self.cfg.dist_root_url, toolchain) + .await + .map_err(|err| match err.downcast_ref::() { + Some(RustupError::ChecksumFailed { .. }) => err, + Some(RustupError::DownloadNotExists { .. }) => { + DistError::MissingReleaseForToolchain(toolchain.manifest_name()).into() + } + _ => err.context(format!( + "failed to download manifest for '{}'", toolchain.manifest_name() - )); - } - _ => { - return Err(err).with_context(|| { - format!( - "failed to download manifest for '{}'", - toolchain.manifest_name() - ) - }); - } - }, - }; - - let result = manifestation - .update_v1(&manifest, update_hash, download) - .await; - - // inspect, determine what context to add, then process afterwards. - if let Err(e) = &result - && let Some(RustupError::DownloadNotExists { .. }) = e.downcast_ref::() - { - return result.with_context(|| { - format!("could not download nonexistent rust version `{toolchain_str}`") - }); + )), + })?; + + let result = manifestation + .update_v1(&manifest, &self.update_hash, download) + .await; + + // inspect, determine what context to add, then process afterwards. + if let Err(e) = &result + && let Some(RustupError::DownloadNotExists { .. }) = e.downcast_ref::() + { + return result.with_context(|| { + format!("could not download nonexistent rust version `{toolchain}`") + }); + } + + result } - result + pub(crate) async fn dl_v2_manifest( + &self, + prefix: &InstallPrefix, + toolchain: &ToolchainDesc, + ) -> Result> { + self.dl_cfg + .dl_v2_manifest( + // Skip the update hash when the installed manifest is missing or when components + // or targets were requested, since either case still requires the channel manifest. + (prefix.dist_manifest().is_some() + && self.components.is_empty() + && self.targets.is_empty()) + .then_some(&self.update_hash), + toolchain, + self.cfg, + ) + .await + } } fn date_from_manifest_date(date_str: &str) -> Option { diff --git a/tests/suite/cli_rustup_ui/rustup_up_cmd_help_flag.stdout.term.svg b/tests/suite/cli_rustup_ui/rustup_up_cmd_help_flag.stdout.term.svg index 3d78c43eaa..79609dd997 100644 --- a/tests/suite/cli_rustup_ui/rustup_up_cmd_help_flag.stdout.term.svg +++ b/tests/suite/cli_rustup_ui/rustup_up_cmd_help_flag.stdout.term.svg @@ -1,4 +1,4 @@ - +