diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index e45fff7a05..9da2253c2b 100644 --- a/doc/user-guide/src/environment-variables.md +++ b/doc/user-guide/src/environment-variables.md @@ -47,6 +47,9 @@ - `RUSTUP_NO_BACKTRACE`. Disables backtraces on non-panic errors even when `RUST_BACKTRACE` is set. +- `RUSTUP_RELEASE_HINT` (default: `1`). When set to `0`, suppresses the hint + that is shown when a new stable Rust release is (likely) available. + - `RUSTUP_PERMIT_COPY_RENAME` *unstable*. When set, allows rustup to fall-back to copying files if attempts to `rename` result in cross-device link errors. These errors occur on OverlayFS, which is used by [Docker][dc]. This diff --git a/src/cli/common.rs b/src/cli/common.rs index cf0257c958..0924914558 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -4,7 +4,9 @@ use std::fmt::Display; use std::fs; use std::io::{BufRead, Write}; use std::path::Path; +use std::str::FromStr; use std::sync::LazyLock; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{cmp, env}; use anstyle::Style; @@ -17,15 +19,18 @@ use tracing_subscriber::{EnvFilter, Registry, reload::Handle}; use crate::{ config::Cfg, - dist::{DistOptions, TargetTuple, ToolchainDesc}, + dist::{DistOptions, PartialToolchainDesc, TargetTuple, ToolchainDesc}, errors::RustupError, install::{InstallMethod, UpdateStatus}, process::Process, - toolchain::{LocalToolchainName, Toolchain, ToolchainName}, + toolchain::{DistributableToolchain, LocalToolchainName, Toolchain, ToolchainName}, utils::{self, ExitCode}, }; pub(crate) const WARN_COMPLETE_PROFILE: &str = "downloading with complete profile isn't recommended unless you are a developer of the rust language"; +const RELEASE_CYCLE_DAYS: i64 = 42; +const NOTIFY_INTERVAL_SECS: u64 = 24 * 60 * 60; +const FALLBACK_RELEASE_DATE: &str = "2026-04-17"; pub(crate) fn confirm(question: &str, default: bool, process: &Process) -> Result { write!(process.stdout().lock(), "{question} ")?; @@ -569,3 +574,61 @@ pub(super) fn update_console_filter( .expect("error reloading `EnvFilter` for console_logger"); } } + +/// Notifies a user with a hint whenever a new Rust release is available. +/// This is only shown at max once per day and only if not in proxy mode. +pub(crate) fn notify_release(cfg: &Cfg<'_>) -> Result<()> { + if cfg.process.var("RUSTUP_RELEASE_HINT").ok().as_deref() == Some("0") { + return Ok(()); + } + + let time_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // Limit notifications to at most once per day. + // This is checked before loading the manifest to avoid unnecessary disk I/O. + let last_notified = cfg + .settings_file + .with(|s| Ok(s.last_release_notified_secs.unwrap_or(0)))?; + if time_now.saturating_sub(last_notified) < NOTIFY_INTERVAL_SECS { + return Ok(()); + } + + let default_host = cfg.default_host_tuple()?; + let stable_desc = PartialToolchainDesc::from_str("stable")?.resolve(&default_host)?; + let distributable = DistributableToolchain::new(cfg, stable_desc) + .map_err(|e| anyhow!("stable toolchain unavailable: {e}"))?; + let release_date_str = distributable + .get_manifestation() + .and_then(|m| m.load_manifest()) + .ok() + .flatten() + .map(|m| m.date.clone()) + .unwrap_or_else(|| FALLBACK_RELEASE_DATE.to_owned()); + + let today = chrono::DateTime::from_timestamp(time_now as i64, 0) + .unwrap_or_default() + .date_naive(); + + let release_date = chrono::NaiveDate::parse_from_str(&release_date_str, "%Y-%m-%d") + .map_err(|e| anyhow!("could not parse release date '{}': {e}", release_date_str))?; + + // Skip the hint if fewer than 6 weeks have passed since the last known release. + if (today - release_date).num_days() < RELEASE_CYCLE_DAYS { + return Ok(()); + } + + cfg.settings_file.with_mut(|s| { + s.last_release_notified_secs = Some(time_now); + Ok(()) + })?; + + writeln!( + cfg.process.stderr().lock(), + "hint: a new stable Rust release is available. Run `rustup update stable` to install it." + )?; + + Ok(()) +} diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 17927e1e82..f6b3b0ca8e 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -675,7 +675,13 @@ pub async fn main( )?; cfg.toolchain_override = matches.plus_toolchain; - match subcmd { + let should_notify = !matches.quiet + && !matches!( + subcmd, + RustupSubcmd::Update { .. } | RustupSubcmd::Install { .. } + ); + + let result = match subcmd { RustupSubcmd::DumpTestament => common::dump_testament(process), RustupSubcmd::Install { opts } => update(cfg, opts, true).await, RustupSubcmd::Uninstall { opts } => toolchain_remove(cfg, opts).await, @@ -802,7 +808,13 @@ pub async fn main( RustupSubcmd::Completions { shell, command } => { output_completion_script(shell, command, process) } + }; + + if should_notify { + let _ = common::notify_release(cfg); } + + result } async fn default_( diff --git a/src/settings.rs b/src/settings.rs index 42dc7ceebf..4b2969a485 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -96,6 +96,8 @@ pub struct Settings { pub auto_self_update: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auto_install: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_release_notified_secs: Option, } impl Settings { diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 5fb3987a67..97728a65de 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -584,6 +584,7 @@ async fn rustup_run_not_installed() { .expect(["rustup", "run", "nightly", "rustc", "--version"]) .await .with_stderr(snapbox::str![[r#" +hint: a new stable Rust release is available. Run `rustup update stable` to install it. error: toolchain 'nightly-[HOST_TUPLE]' is not installed help: run `rustup toolchain install nightly-[HOST_TUPLE]` to install it diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 6ceb147e9c..52380646fe 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -870,7 +870,10 @@ installed targets: [HOST_TUPLE] "#]]) - .with_stderr(snapbox::str![[""]]) + .with_stderr(snapbox::str![[r#" +hint: a new stable Rust release is available. Run `rustup update stable` to install it. + +"#]]) .is_ok(); } diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index f821659935..e345e480fa 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -378,6 +378,7 @@ async fn update_exact() { .with_stderr(snapbox::str![[r#" info: checking for self-update (current version: [CURRENT_VERSION]) info: downloading self-update (new version: [TEST_VERSION]) +hint: a new stable Rust release is available. Run `rustup update stable` to install it. "#]]) .is_ok(); @@ -406,6 +407,7 @@ async fn update_precise() { info: checking for self-update (current version: [CURRENT_VERSION]) info: `RUSTUP_VERSION` has been set to `[TEST_VERSION]` info: downloading self-update (new version: [TEST_VERSION]) +hint: a new stable Rust release is available. Run `rustup update stable` to install it. "#]]); } @@ -594,6 +596,7 @@ async fn update_no_change() { "#]]) .with_stderr(snapbox::str![[r#" info: checking for self-update (current version: [CURRENT_VERSION]) +hint: a new stable Rust release is available. Run `rustup update stable` to install it. "#]]) .is_ok(); @@ -1056,6 +1059,7 @@ async fn update_does_not_overwrite_rustfmt() { .with_stderr(snapbox::str![[r#" info: checking for self-update (current version: [CURRENT_VERSION]) warn: tool `rustfmt` is already installed, remove it from `[..]`, then run `rustup update` to have rustup manage this tool. +hint: a new stable Rust release is available. Run `rustup update stable` to install it. "#]]) .is_ok();