Skip to main content

rpi_cli/
updates.rs

1//! rpi self-update and release notification support.
2
3use serde::Deserialize;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct UpdateNotice {
7    pub name: String,
8    pub current: String,
9    pub latest: String,
10    pub command: String,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct UpdateWarning {
15    pub message: String,
16    pub command: String,
17}
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct UpdateReport {
21    pub notices: Vec<UpdateNotice>,
22    pub warnings: Vec<UpdateWarning>,
23}
24
25impl UpdateReport {
26    pub fn is_empty(&self) -> bool {
27        self.notices.is_empty() && self.warnings.is_empty()
28    }
29}
30
31#[derive(Debug, Deserialize)]
32struct CratesResponse {
33    #[serde(rename = "crate")]
34    crate_info: CrateInfo,
35}
36#[derive(Debug, Deserialize)]
37struct CrateInfo {
38    max_version: String,
39}
40
41fn updates_disabled() -> bool {
42    crate::args::offline_env_enabled() || std::env::var_os("RPI_DISABLE_UPDATE_CHECK").is_some()
43}
44
45pub async fn check_rpi_startup() -> UpdateReport {
46    if updates_disabled() {
47        return UpdateReport::default();
48    }
49    let client = match reqwest::Client::builder()
50        .timeout(std::time::Duration::from_secs(2))
51        .user_agent(format!("rpi/{}", crate::VERSION))
52        .build()
53    {
54        Ok(c) => c,
55        Err(_) => return UpdateReport::default(),
56    };
57    let url = "https://crates.io/api/v1/crates/rpi-cli";
58    let latest = match client
59        .get(url)
60        .send()
61        .await
62        .and_then(|r| r.error_for_status())
63    {
64        Ok(response) => match response.json::<CratesResponse>().await {
65            Ok(v) => v.crate_info.max_version,
66            Err(_) => return UpdateReport::default(),
67        },
68        Err(_) => return UpdateReport::default(),
69    };
70    if is_newer(crate::VERSION, &latest) {
71        UpdateReport {
72            notices: vec![UpdateNotice {
73                name: "rpi".into(),
74                current: crate::VERSION.into(),
75                latest,
76                command: "rpi update".into(),
77            }],
78            warnings: Vec::new(),
79        }
80    } else {
81        UpdateReport::default()
82    }
83}
84
85pub fn is_newer(current: &str, latest: &str) -> bool {
86    match (
87        semver::Version::parse(current),
88        semver::Version::parse(latest),
89    ) {
90        (Ok(a), Ok(b)) => b > a,
91        _ => current != latest,
92    }
93}
94
95pub fn print_startup_notices(report: &UpdateReport) {
96    for notice in &report.notices {
97        eprintln!(
98            "update available: {} {} -> {} ({})",
99            notice.name, notice.current, notice.latest, notice.command
100        );
101    }
102    for warning in &report.warnings {
103        eprintln!("warning: {} ({})", warning.message, warning.command);
104    }
105}
106
107pub fn run_self_update(_args: &[String]) -> i32 {
108    let status = std::process::Command::new("cargo")
109        .args(["install", "rpi-cli", "--locked"])
110        .status();
111    match status {
112        Ok(s) if s.success() => 0,
113        Ok(s) => s.code().unwrap_or(1),
114        Err(e) => {
115            eprintln!("rpi update failed: {e}");
116            1
117        }
118    }
119}