Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/user-guide/src/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 65 additions & 2 deletions src/cli/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<bool> {
write!(process.stdout().lock(), "{question} ")?;
Expand Down Expand Up @@ -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| {
Copy link
Copy Markdown
Member

@rami3l rami3l May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it might be harmful to write this in settings_file; I'd suggest if we were to write the timestamp to a file, it should be an individual one so that under XDG we can put it under ~/.local/state/rustup/ whereas the settings file should clearly be under ~/.config/rustup/.

cc @Cloud0310 for potential interaction/collaboration regarding XDG.

View changes since the review

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(())
}
14 changes: 13 additions & 1 deletion src/cli/rustup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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_(
Expand Down
2 changes: 2 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub struct Settings {
pub auto_self_update: Option<SelfUpdateMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_install: Option<AutoInstallMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_release_notified_secs: Option<u64>,
}

impl Settings {
Expand Down
1 change: 1 addition & 0 deletions tests/suite/cli_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion tests/suite/cli_rustup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
4 changes: 4 additions & 0 deletions tests/suite/cli_self_upd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.

"#]]);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading