Skip to content

Commit 51800f7

Browse files
feat(cli): notify when a new stable Rust release is available
1 parent 0d03045 commit 51800f7

6 files changed

Lines changed: 90 additions & 3 deletions

File tree

doc/user-guide/src/environment-variables.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
- `RUSTUP_NO_BACKTRACE`. Disables backtraces on non-panic errors even when
4848
`RUST_BACKTRACE` is set.
4949

50+
- `RUSTUP_RELEASE_HINT` (default: `1`). When set to `1`, shows a hint when a
51+
new stable Rust release is (likely) available. Set this value to `0` to
52+
suppress the hint.
53+
5054
- `RUSTUP_PERMIT_COPY_RENAME` *unstable*. When set, allows rustup to fall-back
5155
to copying files if attempts to `rename` result in cross-device link
5256
errors. These errors occur on OverlayFS, which is used by [Docker][dc]. This

src/cli/common.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ use std::fmt::Display;
44
use std::fs;
55
use std::io::{BufRead, Write};
66
use std::path::Path;
7+
use std::str::FromStr;
78
use std::sync::LazyLock;
9+
use std::time::{SystemTime, UNIX_EPOCH};
810
use std::{cmp, env};
911

1012
use anstyle::Style;
1113
use anyhow::{Context, Result, anyhow};
14+
use chrono::{DateTime, NaiveDate};
1215
use clap_cargo::style::{CONTEXT, ERROR, UPDATE_ADDED, UPDATE_UNCHANGED, UPDATE_UPGRADED};
1316
use futures_util::future::join_all;
1417
use git_testament::{git_testament, render_testament};
@@ -17,15 +20,18 @@ use tracing_subscriber::{EnvFilter, Registry, reload::Handle};
1720

1821
use crate::{
1922
config::Cfg,
20-
dist::{DistOptions, TargetTuple, ToolchainDesc},
23+
dist::{DistOptions, PartialToolchainDesc, TargetTuple, ToolchainDesc},
2124
errors::RustupError,
2225
install::{InstallMethod, UpdateStatus},
2326
process::Process,
24-
toolchain::{LocalToolchainName, Toolchain, ToolchainName},
27+
toolchain::{DistributableToolchain, LocalToolchainName, Toolchain, ToolchainName},
2528
utils::{self, ExitCode},
2629
};
2730

2831
pub(crate) const WARN_COMPLETE_PROFILE: &str = "downloading with complete profile isn't recommended unless you are a developer of the rust language";
32+
const RELEASE_CYCLE_DAYS: i64 = 42;
33+
const NOTIFY_INTERVAL_SECS: u64 = 24 * 60 * 60;
34+
const FALLBACK_RELEASE_DATE: &str = "2026-04-17";
2935

3036
pub(crate) fn confirm(question: &str, default: bool, process: &Process) -> Result<bool> {
3137
write!(process.stdout().lock(), "{question} ")?;
@@ -569,3 +575,61 @@ pub(super) fn update_console_filter(
569575
.expect("error reloading `EnvFilter` for console_logger");
570576
}
571577
}
578+
579+
/// Notifies a user with a hint whenever a new Rust release is available.
580+
/// This is only shown at max once per day and only if not in proxy mode.
581+
pub(crate) fn notify_release(cfg: &Cfg<'_>) -> Result<()> {
582+
if cfg.process.var("RUSTUP_RELEASE_HINT").ok().as_deref() == Some("0") {
583+
return Ok(());
584+
}
585+
586+
let time_now = SystemTime::now()
587+
.duration_since(UNIX_EPOCH)
588+
.unwrap_or_default()
589+
.as_secs();
590+
591+
// Limit notifications to at most once per day.
592+
// This is checked before loading the manifest to avoid unnecessary disk I/O.
593+
let last_notified = cfg
594+
.settings_file
595+
.with(|s| Ok(s.last_release_notified_secs.unwrap_or(0)))?;
596+
if time_now.saturating_sub(last_notified) < NOTIFY_INTERVAL_SECS {
597+
return Ok(());
598+
}
599+
600+
let default_host = cfg.default_host_tuple()?;
601+
let stable_desc = PartialToolchainDesc::from_str("stable")?.resolve(&default_host)?;
602+
let distributable = DistributableToolchain::new(cfg, stable_desc)
603+
.map_err(|e| anyhow!("stable toolchain unavailable: {e}"))?;
604+
let release_date_str = match distributable.get_manifestation() {
605+
Ok(manifestation) => match manifestation.load_manifest() {
606+
Ok(Some(manifest)) => manifest.date,
607+
Ok(None) | Err(_) => FALLBACK_RELEASE_DATE.to_owned(),
608+
},
609+
Err(_) => FALLBACK_RELEASE_DATE.to_owned(),
610+
};
611+
612+
let today = DateTime::from_timestamp(time_now as i64, 0)
613+
.unwrap_or_default()
614+
.date_naive();
615+
616+
let release_date = NaiveDate::parse_from_str(&release_date_str, "%Y-%m-%d")
617+
.map_err(|e| anyhow!("could not parse release date '{}': {e}", release_date_str))?;
618+
619+
// Skip the hint if fewer than 6 weeks have passed since the last known release.
620+
if (today - release_date).num_days() < RELEASE_CYCLE_DAYS {
621+
return Ok(());
622+
}
623+
624+
cfg.settings_file.with_mut(|s| {
625+
s.last_release_notified_secs = Some(time_now);
626+
Ok(())
627+
})?;
628+
629+
writeln!(
630+
cfg.process.stderr().lock(),
631+
"hint: a new stable Rust release is available. Run `rustup update stable` to install it."
632+
)?;
633+
634+
Ok(())
635+
}

src/cli/rustup_mode.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,12 @@ pub async fn main(
707707

708708
let should_warn = subcmd.should_warn_empty_setup();
709709

710+
let should_notify = !matches.quiet
711+
&& !matches!(
712+
subcmd,
713+
RustupSubcmd::Update { .. } | RustupSubcmd::Install { .. }
714+
);
715+
710716
let exit_code = match subcmd {
711717
RustupSubcmd::DumpTestament => common::dump_testament(process),
712718
RustupSubcmd::Install { opts } => update(cfg, opts, true).await,
@@ -840,6 +846,10 @@ pub async fn main(
840846
warn!("no toolchain installed and no default toolchain set\n{DEFAULT_STABLE_HINT}");
841847
}
842848

849+
if should_notify {
850+
let _ = common::notify_release(cfg);
851+
}
852+
843853
Ok(exit_code)
844854
}
845855

src/settings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ pub struct Settings {
9696
pub auto_self_update: Option<SelfUpdateMode>,
9797
#[serde(skip_serializing_if = "Option::is_none")]
9898
pub auto_install: Option<AutoInstallMode>,
99+
#[serde(skip_serializing_if = "Option::is_none")]
100+
pub last_release_notified_secs: Option<u64>,
99101
}
100102

101103
impl Settings {

tests/suite/cli_rustup.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,10 @@ installed targets:
880880
[HOST_TUPLE]
881881
882882
"#]])
883-
.with_stderr(snapbox::str![[""]])
883+
.with_stderr(snapbox::str![[r#"
884+
hint: a new stable Rust release is available. Run `rustup update stable` to install it.
885+
886+
"#]])
884887
.is_ok();
885888
}
886889

tests/suite/cli_self_upd.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ async fn update_exact() {
434434
.with_stderr(snapbox::str![[r#"
435435
info: checking for self-update (current version: [CURRENT_VERSION])
436436
info: downloading self-update (new version: [TEST_VERSION])
437+
hint: a new stable Rust release is available. Run `rustup update stable` to install it.
437438
438439
"#]])
439440
.is_ok();
@@ -462,6 +463,7 @@ async fn update_precise() {
462463
info: checking for self-update (current version: [CURRENT_VERSION])
463464
info: `RUSTUP_VERSION` has been set to `[TEST_VERSION]`
464465
info: downloading self-update (new version: [TEST_VERSION])
466+
hint: a new stable Rust release is available. Run `rustup update stable` to install it.
465467
466468
"#]]);
467469
}
@@ -650,6 +652,7 @@ async fn update_no_change() {
650652
"#]])
651653
.with_stderr(snapbox::str![[r#"
652654
info: checking for self-update (current version: [CURRENT_VERSION])
655+
hint: a new stable Rust release is available. Run `rustup update stable` to install it.
653656
654657
"#]])
655658
.is_ok();
@@ -1112,6 +1115,7 @@ async fn update_does_not_overwrite_rustfmt() {
11121115
.with_stderr(snapbox::str![[r#"
11131116
info: checking for self-update (current version: [CURRENT_VERSION])
11141117
warn: tool `rustfmt` is already installed, remove it from `[..]`, then run `rustup update` to have rustup manage this tool.
1118+
hint: a new stable Rust release is available. Run `rustup update stable` to install it.
11151119
11161120
"#]])
11171121
.is_ok();

0 commit comments

Comments
 (0)