soma_observability/
binary_status.rs1use std::ffi::OsStr;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::time::SystemTime;
10
11#[cfg(test)]
12#[path = "binary_status_tests.rs"]
13mod tests;
14
15const SOURCE_INPUTS: &[&str] = &[
16 "Cargo.toml",
17 "Cargo.lock",
18 "rust-toolchain.toml",
19 ".cargo/config.toml",
20 "src",
21 "config",
22 "migrations",
23 "assets",
24 "apps/web/out",
25];
26
27pub fn stale_binary_warning() -> Option<String> {
28 if std::env::var_os("SOMA_SUPPRESS_STALE_BINARY_WARNING").is_some() {
29 return None;
30 }
31
32 let exe = std::env::current_exe().ok()?;
33 let binary_mtime = fs::metadata(&exe).ok()?.modified().ok()?;
34 stale_binary_warning_at(binary_mtime, Path::new(env!("CARGO_MANIFEST_DIR")))
35}
36
37fn stale_binary_warning_at(binary_mtime: SystemTime, source_root: &Path) -> Option<String> {
38 if !source_root.is_dir() {
39 return None;
40 }
41
42 let newest = SOURCE_INPUTS
43 .iter()
44 .filter_map(|relative| {
45 newest_mtime_for_path(source_root.join(relative)).map(|mtime| (*relative, mtime))
46 })
47 .max_by_key(|(_, mtime)| *mtime)?;
48
49 stale_binary_warning_for(binary_mtime, [newest])
50}
51
52pub fn stale_binary_warning_for<'a>(
53 binary_mtime: SystemTime,
54 inputs: impl IntoIterator<Item = (&'a str, SystemTime)>,
55) -> Option<String> {
56 inputs
57 .into_iter()
58 .filter(|(_, input_mtime)| *input_mtime > binary_mtime)
59 .max_by_key(|(_, input_mtime)| *input_mtime)
60 .map(|(path, _)| warning_message(path))
61}
62
63pub fn warning_message(newer_input: &str) -> String {
64 format!(
65 "outdated soma binary: {newer_input} is newer than the running executable. Rebuild with `cargo build --release --bin soma` and install with `just install-local`."
66 )
67}
68
69fn newest_mtime_for_path(path: PathBuf) -> Option<SystemTime> {
70 let metadata = fs::metadata(&path).ok()?;
71 if metadata.is_file() {
72 return metadata.modified().ok();
73 }
74 if !metadata.is_dir() {
75 return None;
76 }
77
78 let mut newest = metadata.modified().ok();
79 let mut stack = vec![path];
80 while let Some(dir) = stack.pop() {
81 let Ok(entries) = fs::read_dir(dir) else {
82 continue;
83 };
84 for entry in entries.flatten() {
85 let path = entry.path();
86 if should_skip_path(&path) {
87 continue;
88 }
89 let Ok(metadata) = entry.metadata() else {
90 continue;
91 };
92 if metadata.is_dir() {
93 stack.push(path);
94 } else if metadata.is_file() {
95 if let Ok(mtime) = metadata.modified() {
96 if newest.is_none_or(|current| mtime > current) {
97 newest = Some(mtime);
98 }
99 }
100 }
101 }
102 }
103 newest
104}
105
106fn should_skip_path(path: &Path) -> bool {
107 path.file_name()
108 .and_then(OsStr::to_str)
109 .is_some_and(|name| matches!(name, ".git" | "target" | "node_modules"))
110}