Skip to main content

tsafe_core/
update.rs

1//! Optional self-update check against a ProGet Universal Package feed.
2//!
3//! All functions are no-ops (return `None` / `Ok(())`) when the
4//! `PROGET_BASE_URL` env var is unset or the request fails, so the caller
5//! never needs to handle update-check failures as hard errors.
6
7/// The version of this build. At runtime, prefers `TSAFE_CLI_VERSION` (set
8/// by the CLI binary before launching the TUI) so the UI always shows the
9/// installed binary version rather than the tsafe-core crate version.
10pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
11
12/// Resolve the effective binary version for display: CLI version if injected,
13/// otherwise the compiled-in tsafe-core version.
14pub fn display_version() -> &'static str {
15    // SAFETY: env var is set once at process start before any threads spawn.
16    // We leak the String to get a 'static str for display convenience.
17    std::env::var("TSAFE_CLI_VERSION")
18        .ok()
19        .map(|v| -> &'static str { Box::leak(v.into_boxed_str()) })
20        .unwrap_or(PKG_VERSION)
21}
22
23/// Query ProGet for the latest Universal Package version (white-label; configure feed/package via env).
24///
25/// Returns `Some(version_string)` if a newer version is available, `None`
26/// otherwise. Silently returns `None` when `PROGET_BASE_URL` is not set or
27/// the request fails — callers should treat `None` as "no update info".
28pub fn check_for_update() -> Option<String> {
29    let base_url = std::env::var("PROGET_BASE_URL").ok()?;
30    if !base_url.starts_with("https://") {
31        return None;
32    }
33    let feed = std::env::var("PROGET_FEED").unwrap_or_else(|_| "tsafe".to_string());
34    let pkg = std::env::var("PROGET_PACKAGE_NAME").unwrap_or_else(|_| "tsafe".to_string());
35    let url = format!("{base_url}/upack/{feed}/versions?packageName={pkg}&count=1");
36
37    let agent = ureq::AgentBuilder::new()
38        .timeout_connect(std::time::Duration::from_secs(3))
39        .timeout(std::time::Duration::from_secs(5))
40        .build();
41
42    let json: serde_json::Value = agent.get(&url).call().ok()?.into_json().ok()?;
43
44    // ProGet returns a JSON array; the first element is the latest version.
45    // The version field is lowercase in ProGet v5+ but may be "Version" on older.
46    let entry = json.as_array()?.first()?;
47    let latest = entry
48        .get("version")
49        .or_else(|| entry.get("Version"))
50        .and_then(|v| v.as_str())?
51        .to_string();
52
53    if is_newer(&latest, PKG_VERSION) {
54        Some(latest)
55    } else {
56        None
57    }
58}
59
60/// Returns `true` if `candidate` is a strictly higher semver than `current`.
61fn is_newer(candidate: &str, current: &str) -> bool {
62    fn parse(v: &str) -> (u64, u64, u64) {
63        let mut p = v.trim_start_matches('v').split('.');
64        let major: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
65        let minor: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
66        let patch: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
67        (major, minor, patch)
68    }
69    parse(candidate) > parse(current)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::is_newer;
75
76    #[test]
77    fn newer_minor_detected() {
78        assert!(is_newer("0.2.0", "0.1.0"));
79    }
80
81    #[test]
82    fn newer_major_detected() {
83        assert!(is_newer("1.0.0", "0.9.9"));
84    }
85
86    #[test]
87    fn newer_patch_detected() {
88        assert!(is_newer("0.1.1", "0.1.0"));
89    }
90
91    #[test]
92    fn same_version_not_newer() {
93        assert!(!is_newer("0.1.0", "0.1.0"));
94    }
95
96    #[test]
97    fn older_version_not_newer() {
98        assert!(!is_newer("0.0.9", "0.1.0"));
99    }
100
101    #[test]
102    fn v_prefix_stripped() {
103        assert!(is_newer("v0.2.0", "0.1.0"));
104        assert!(!is_newer("v0.1.0", "0.1.0"));
105    }
106}