Skip to main content

ggen_cli_lib/
version_checker.rs

1use colored::Colorize;
2use std::fs;
3use std::io::IsTerminal;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7/// Checks if there is a newer compiled ggen binary in the target directory compared to the running one,
8/// and prints a warning if so.
9pub fn check_outdated_binary() {
10    // Check if GGEN_SKIP_OUTDATED_WARNING or standard CI bypass variables are present. If so, return early.
11    if std::env::var_os("GGEN_SKIP_OUTDATED_WARNING").is_some() {
12        return;
13    }
14    for var in &[
15        "CI",
16        "GITHUB_ACTIONS",
17        "TRAVIS",
18        "CIRCLECI",
19        "GITLAB_CI",
20        "JENKINS_URL",
21    ] {
22        if std::env::var_os(var).is_some() {
23            return;
24        }
25    }
26
27    // Check if !std::io::stderr().is_terminal(). If so, return early.
28    // (Bypassed if GGEN_TEST_FORCE_TERMINAL is set to "1" for integration tests)
29    if !std::io::stderr().is_terminal()
30        && std::env::var("GGEN_TEST_FORCE_TERMINAL").as_deref() != Ok("1")
31    {
32        return;
33    }
34
35    // Retrieve the current executable path via std::env::current_exe(). If this fails, return early.
36    let current_exe = match std::env::current_exe() {
37        Ok(path) => path,
38        Err(_) => return,
39    };
40
41    let current_exe_canonical = current_exe
42        .canonicalize()
43        .unwrap_or_else(|_| current_exe.clone());
44
45    let running_metadata = match fs::metadata(&current_exe_canonical) {
46        Ok(meta) => meta,
47        Err(_) => return,
48    };
49
50    let running_mtime = match running_metadata.modified() {
51        Ok(mtime) => mtime,
52        Err(_) => return,
53    };
54
55    // Find the cargo target folder and workspace root. Walk up parent directories of current directory or the current executable looking for Cargo.toml and a target/ directory.
56    let (workspace_root, target_dir) =
57        match find_workspace_root_and_target_dir(&current_exe_canonical) {
58            Some(paths) => paths,
59            None => return,
60        };
61
62    // Check <workspace_root>/target/debug/ggen and <workspace_root>/target/release/ggen (or ggen.exe on Windows).
63    let bin_name = if cfg!(windows) { "ggen.exe" } else { "ggen" };
64    let debug_bin = target_dir.join("debug").join(bin_name);
65    let release_bin = target_dir.join("release").join(bin_name);
66
67    let mut candidates = Vec::new();
68    if debug_bin.is_file() {
69        candidates.push(debug_bin);
70    }
71    if release_bin.is_file() {
72        candidates.push(release_bin);
73    }
74
75    let mut newest_path = None;
76    let mut newest_mtime = running_mtime;
77
78    for candidate in candidates {
79        // Ensure candidate is not the running binary
80        if let Ok(cand_canonical) = candidate.canonicalize() {
81            if cand_canonical == current_exe_canonical {
82                continue;
83            }
84        } else {
85            continue;
86        }
87
88        if let Ok(meta) = fs::metadata(&candidate) {
89            if let Ok(mtime) = meta.modified() {
90                if mtime > newest_mtime {
91                    newest_mtime = mtime;
92                    newest_path = Some(candidate);
93                }
94            }
95        }
96    }
97
98    // If a target binary has a strictly newer mtime and is not the running binary, print a warning to stderr
99    if let Some(latest_path) = newest_path {
100        if let Ok(diff) = newest_mtime.duration_since(running_mtime) {
101            let duration_str = format_duration(diff);
102            eprintln!(
103                "{}: running an outdated 'ggen' binary",
104                "warning".yellow().bold()
105            );
106            eprintln!(
107                "   --> current: {} (compiled {} older)",
108                current_exe_canonical.display(),
109                duration_str
110            );
111            eprintln!("   --> latest:  {}", latest_path.display());
112            eprintln!("{}: compile the latest changes or update your installation with 'cargo install --path {}'", "info".green().bold(), workspace_root.display());
113        }
114    }
115}
116
117fn find_workspace_root_and_target_dir(current_exe: &Path) -> Option<(PathBuf, PathBuf)> {
118    // 1. Walk up from the current executable parent
119    let mut current = current_exe.parent();
120    while let Some(path) = current {
121        let target_dir = path.join("target");
122        if path.join("Cargo.toml").exists() && target_dir.is_dir() {
123            return Some((path.to_path_buf(), target_dir));
124        }
125        current = path.parent();
126    }
127
128    // 2. Walk up from the current working directory
129    if let Ok(cwd) = std::env::current_dir() {
130        let mut current = Some(cwd.as_path());
131        while let Some(path) = current {
132            let target_dir = path.join("target");
133            if path.join("Cargo.toml").exists() && target_dir.is_dir() {
134                return Some((path.to_path_buf(), target_dir));
135            }
136            current = path.parent();
137        }
138    }
139
140    None
141}
142
143fn format_duration(d: std::time::Duration) -> String {
144    let secs = d.as_secs();
145    if secs < 60 {
146        format!("{}s", secs)
147    } else if secs < 3600 {
148        format!("{}m", secs / 60)
149    } else if secs < 86400 {
150        let hours = secs / 3600;
151        let mins = (secs % 3600) / 60;
152        if mins > 0 {
153            format!("{}h {}m", hours, mins)
154        } else {
155            format!("{}h", hours)
156        }
157    } else {
158        let days = secs / 86400;
159        let hours = (secs % 86400) / 3600;
160        if hours > 0 {
161            format!("{}d {}h", days, hours)
162        } else {
163            format!("{}d", days)
164        }
165    }
166}