diff --git a/src/cli/common.rs b/src/cli/common.rs index 1db6bd80e8..11f8f698e5 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -10,6 +10,7 @@ use std::{cmp, env}; use anstyle::Style; use anyhow::{Context, Result, anyhow}; use clap_cargo::style::{CONTEXT, ERROR, UPDATE_ADDED, UPDATE_UNCHANGED, UPDATE_UPGRADED}; +use futures_util::future::join_all; use git_testament::{git_testament, render_testament}; use tracing::{error, info, warn}; use tracing_subscriber::{EnvFilter, Registry, reload::Handle}; @@ -213,11 +214,26 @@ fn show_channel_updates( pub(crate) async fn update_all_channels(cfg: &Cfg<'_>, force_update: bool) -> Result { let profile = cfg.get_profile()?; + let channels = cfg.list_channels()?; + + // Run a pre-check to determine which channels have updates available. + let channels_with_manifests = join_all(channels.into_iter().map(|(desc, d)| async move { + // Prefetch errors are intentionally discarded; a failed prefetch falls + // back to the normal update path, where errors are properly propagated. + let manifest = d.fetch_dist_manifest().await.unwrap_or(None); + (desc, d, manifest) + })) + .await; + let mut toolchains = Vec::new(); - for (desc, distributable) in cfg.list_channels()? { - let options = DistOptions::new(&[], &[], &desc, profile, force_update, cfg)? - .for_update(&distributable, false); - let result = InstallMethod::Dist(options).install().await; + for (desc, distributable, manifest) in channels_with_manifests { + let result = if force_update || manifest.is_some() { + let options = DistOptions::new(&[], &[], &desc, profile, force_update, cfg)? + .for_update(&distributable, false); + InstallMethod::Dist(options).install(manifest).await + } else { + Ok(UpdateStatus::Unchanged) + }; if let Err(e) = &result { error!("{e}"); diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index d1b438820c..484a31bd17 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -43,7 +43,7 @@ use crate::{ dist::{ AutoInstallMode, DistOptions, PartialToolchainDesc, Profile, TargetTriple, download::DownloadCfg, - manifest::{Component, ComponentStatus}, + manifest::{Component, ComponentStatus, ManifestWithHash}, }, errors::RustupError, install::{InstallMethod, UpdateStatus}, @@ -860,7 +860,12 @@ async fn check_updates(cfg: &Cfg<'_>, opts: CheckOpts) -> Result { async move { let _permit = sem.acquire().await.unwrap(); let current_version = distributable.show_version()?; - let dist_version = distributable.show_dist_version().await?; + let dist_version = match distributable.fetch_dist_manifest().await? { + Some(ManifestWithHash { manifest, .. }) => { + Some(manifest.get_rust_version()?.to_string()) + } + None => None, + }; let mut update_a = false; let template = match (current_version, dist_version) { @@ -998,7 +1003,7 @@ async fn update( Ok(d) => { if !opts.no_update { InstallMethod::Dist(dist_opts.for_update(&d, opts.allow_downgrade)) - .install() + .install(None) .await? } else { UpdateStatus::Unchanged @@ -1496,10 +1501,10 @@ async fn toolchain_link(cfg: &Cfg<'_>, dest: &CustomToolchainName, src: &Path) - dest, cfg, } - .install() + .install(None) .await?; } else { - InstallMethod::Copy { src, dest, cfg }.install().await?; + InstallMethod::Copy { src, dest, cfg }.install(None).await?; } Ok(ExitCode::SUCCESS) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 483fa42d60..0abe29d906 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -1010,7 +1010,7 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< // For now, we error. let toolchain = DistributableToolchain::new(cfg, desc.clone())?; InstallMethod::Dist(options.for_update(&toolchain, false)) - .install() + .install(None) .await? } else { DistributableToolchain::install(options).await?.0 diff --git a/src/config.rs b/src/config.rs index c7256339fb..fdb45a0d9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -811,7 +811,7 @@ impl<'a> Cfg<'a> { let status = if !distributable.components_exist(&components, &targets)? { options.force = true; InstallMethod::Dist(options.for_update(&distributable, false)) - .install() + .install(None) .await? } else { UpdateStatus::Unchanged diff --git a/src/dist/download.rs b/src/dist/download.rs index e10f027854..9c145b22c8 100644 --- a/src/dist/download.rs +++ b/src/dist/download.rs @@ -13,7 +13,7 @@ use tracing::{debug, info, warn}; use url::Url; use crate::config::Cfg; -use crate::dist::manifest::Manifest; +use crate::dist::manifest::{Manifest, ManifestWithHash}; use crate::dist::{Channel, DEFAULT_DIST_SERVER, ToolchainDesc, temp}; use crate::download::{download_file, download_file_with_resume, is_network_failure}; use crate::errors::RustupError; @@ -153,7 +153,7 @@ impl<'a> DownloadCfg<'a> { update_hash: Option<&Path>, toolchain: &ToolchainDesc, cfg: &Cfg<'_>, - ) -> Result> { + ) -> Result> { let manifest_url = toolchain.manifest_v2_url(&cfg.dist_root_url, self.process); match self .download_and_check(&manifest_url, update_hash, None, ".toml") @@ -161,7 +161,7 @@ impl<'a> DownloadCfg<'a> { { Ok(manifest_dl) => { // Downloaded ok! - let Some((manifest_file, manifest_hash)) = manifest_dl else { + let Some((manifest_file, hash)) = manifest_dl else { return Ok(None); }; let manifest_str = utils::read_file("manifest", &manifest_file)?; @@ -171,7 +171,7 @@ impl<'a> DownloadCfg<'a> { path: manifest_file.to_path_buf(), })?; - Ok(Some((manifest, manifest_hash))) + Ok(Some(ManifestWithHash { manifest, hash })) } Err(any) => { if let Some(err @ RustupError::ChecksumFailed { .. }) = diff --git a/src/dist/manifest.rs b/src/dist/manifest.rs index b58bf0341b..c16d62d22c 100644 --- a/src/dist/manifest.rs +++ b/src/dist/manifest.rs @@ -28,6 +28,12 @@ use crate::{ toolchain::DistributableToolchain, }; +/// A download manifest paired with its respective hash, as returned by the dist server. +pub struct ManifestWithHash { + pub manifest: Manifest, + pub hash: String, +} + /// Used by the `installed_components` function pub(crate) struct ComponentStatus { pub component: Component, diff --git a/src/dist/mod.rs b/src/dist/mod.rs index 0bda9963fe..9dcc23706e 100644 --- a/src/dist/mod.rs +++ b/src/dist/mod.rs @@ -34,7 +34,7 @@ pub mod download; use download::DownloadCfg; pub mod manifest; -use manifest::{Component, Manifest as ManifestV2}; +use manifest::{Component, Manifest as ManifestV2, ManifestWithHash}; pub mod manifestation; use manifestation::{Changes, Manifestation, UpdateStatus}; @@ -955,7 +955,11 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { // // Returns the manifest's hash if anything changed. #[tracing::instrument(level = "trace", err(level = "trace"), skip_all, fields(profile = ?self.profile, prefix = %prefix.path().display()))] - pub(crate) async fn install_into(&self, prefix: &InstallPrefix) -> Result> { + pub(crate) async fn install_into( + &self, + prefix: &InstallPrefix, + 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 if fresh_install && self.update_hash.exists() { @@ -1013,6 +1017,7 @@ 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, @@ -1028,6 +1033,7 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> { self.targets, &mut fetched, self.cfg, + prefetched_manifest.take(), ) .await; @@ -1126,28 +1132,33 @@ async fn try_update_from_dist_( 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}"); - match download - .dl_v2_manifest( - // Even if manifest has not changed, we must continue to install requested components. - // So if components or targets is not empty, we skip passing `update_hash` so that - // we essentially degenerate to `rustup component add` / `rustup target add` - if components.is_empty() && targets.is_empty() { - Some(update_hash) - } else { - None - }, - toolchain, - cfg, - ) - .await - { - Ok(Some((m, hash))) => { + let manifest_result = if prefetched_manifest.is_some() { + Ok(prefetched_manifest) + } else { + download + .dl_v2_manifest( + // Even if manifest has not changed, we must continue to install requested components. + // So if components or targets is not empty, we skip passing `update_hash` so that + // we essentially degenerate to `rustup component add` / `rustup target add` + if 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), diff --git a/src/install.rs b/src/install.rs index 74b0f9bd16..b7f4df76b9 100644 --- a/src/install.rs +++ b/src/install.rs @@ -7,7 +7,7 @@ use tracing::debug; use crate::{ config::Cfg, - dist::{DistOptions, prefix::InstallPrefix}, + dist::{DistOptions, manifest::ManifestWithHash, prefix::InstallPrefix}, errors::RustupError, toolchain::{CustomToolchainName, LocalToolchainName, Toolchain}, utils, @@ -37,7 +37,7 @@ pub(crate) enum InstallMethod<'cfg, 'a> { impl InstallMethod<'_, '_> { // Install a toolchain #[tracing::instrument(level = "trace", err(level = "trace"), skip_all)] - pub(crate) async fn install(self) -> Result { + pub(crate) async fn install(self, manifest: Option) -> Result { // Initialize rayon for use by the remove_dir_all crate limiting the number of threads. // This will error if rayon is already initialized but it's fine to ignore that. let _ = rayon::ThreadPoolBuilder::new() @@ -54,7 +54,7 @@ impl InstallMethod<'_, '_> { } debug!("toolchain directory: {}", self.dest_path().display()); - let updated = self.run(&self.dest_path()).await?; + let updated = self.run(&self.dest_path(), manifest).await?; let status = match updated { false => { @@ -82,7 +82,7 @@ impl InstallMethod<'_, '_> { } } - async fn run(&self, path: &Path) -> Result { + async fn run(&self, path: &Path, manifest: Option) -> Result { if path.exists() { // Don't uninstall first for Dist method match self { @@ -104,7 +104,7 @@ impl InstallMethod<'_, '_> { } InstallMethod::Dist(opts) => { let prefix = &InstallPrefix::from(path.to_owned()); - let maybe_new_hash = opts.install_into(prefix).await?; + let maybe_new_hash = opts.install_into(prefix, manifest).await?; if let Some(hash) = maybe_new_hash { utils::write_file("update hash", &opts.update_hash, &hash)?; diff --git a/src/toolchain/distributable.rs b/src/toolchain/distributable.rs index 74f36b5f28..a82d8e2da2 100644 --- a/src/toolchain/distributable.rs +++ b/src/toolchain/distributable.rs @@ -14,7 +14,7 @@ use crate::{ DistOptions, PartialToolchainDesc, ToolchainDesc, config::Config, download::DownloadCfg, - manifest::{Component, ComponentStatus, Manifest}, + manifest::{Component, ComponentStatus, Manifest, ManifestWithHash}, manifestation::{Changes, Manifestation}, prefix::InstallPrefix, }, @@ -39,7 +39,7 @@ impl<'a> DistributableToolchain<'a> { options: DistOptions<'a, '_>, ) -> anyhow::Result<(UpdateStatus, Self)> { let (cfg, toolchain) = (options.cfg, options.toolchain); - let status = InstallMethod::Dist(options).install().await?; + let status = InstallMethod::Dist(options).install(None).await?; Ok((status, Self::new(cfg, toolchain.clone())?)) } @@ -445,18 +445,14 @@ impl<'a> DistributableToolchain<'a> { Ok(()) } - pub async fn show_dist_version(&self) -> anyhow::Result> { - match DownloadCfg::new(self.toolchain.cfg) + pub async fn fetch_dist_manifest(&self) -> anyhow::Result> { + DownloadCfg::new(self.toolchain.cfg) .dl_v2_manifest( Some(&self.toolchain.cfg.get_hash_file(&self.desc, false)?), &self.desc, self.toolchain.cfg, ) - .await? - { - Some((manifest, _)) => Ok(Some(manifest.get_rust_version()?.to_string())), - None => Ok(None), - } + .await } pub fn show_version(&self) -> anyhow::Result> { diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 229c5650b3..18eb63c305 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -97,7 +97,6 @@ async fn rustup_stable_no_change() { "#]]) .with_stderr(snapbox::str![[r#" -info: syncing channel updates for stable-[HOST_TRIPLE] info: cleaning up downloads & tmp directories "#]]) @@ -210,7 +209,6 @@ info: syncing channel updates for stable-[HOST_TRIPLE] info: latest update on 2015-01-02 for version 1.1.0 (hash-stable-1.1.0) info: removing previous version of component cargo ... -info: syncing channel updates for beta-[HOST_TRIPLE] info: syncing channel updates for nightly-[HOST_TRIPLE] info: latest update on 2015-01-02 for version 1.3.0 (hash-nightly-2) info: removing previous version of component cargo diff --git a/tests/suite/cli_rustup_ui/rustup_update_no_change.stderr.term.svg b/tests/suite/cli_rustup_ui/rustup_update_no_change.stderr.term.svg index 5ce55154fc..e974042abe 100644 --- a/tests/suite/cli_rustup_ui/rustup_update_no_change.stderr.term.svg +++ b/tests/suite/cli_rustup_ui/rustup_update_no_change.stderr.term.svg @@ -1,4 +1,4 @@ - +