ggen_cli_lib/
version_checker.rs1use colored::Colorize;
2use std::fs;
3use std::io::IsTerminal;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7pub fn check_outdated_binary() {
10 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 if !std::io::stderr().is_terminal()
30 && std::env::var("GGEN_TEST_FORCE_TERMINAL").as_deref() != Ok("1")
31 {
32 return;
33 }
34
35 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(¤t_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 let (workspace_root, target_dir) =
57 match find_workspace_root_and_target_dir(¤t_exe_canonical) {
58 Some(paths) => paths,
59 None => return,
60 };
61
62 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 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 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 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 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}