Skip to main content

browser_automation_cli/
install.rs

1//! Chrome cache discovery for one-shot launches (no download / no apt).
2//!
3//! PRD: system Chrome/Chromium only — no embedded BrowserFetcher in MVP.
4
5use std::fs;
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8
9/// Cache directory for optional pre-placed Chrome builds (XDG cache).
10///
11/// Path: `$XDG_CACHE_HOME/browser-automation-cli/browsers` (via `directories`).
12pub fn get_browsers_dir() -> PathBuf {
13    crate::xdg::browsers_dir().unwrap_or_else(|_| {
14        std::env::temp_dir()
15            .join("browser-automation-cli")
16            .join("browsers")
17    })
18}
19
20/// Find a Chrome binary previously placed under the product browsers cache.
21pub fn find_installed_chrome() -> Option<PathBuf> {
22    let browsers_dir = get_browsers_dir();
23    // Debug diagnostics only when stderr is a terminal and --debug was used by the process
24    // (tracing layer). Avoid reading product env vars at runtime.
25    let debug = false;
26
27    if debug {
28        let _ = writeln!(
29            io::stderr(),
30            "[chrome-search] browsers_dir={}",
31            browsers_dir.display()
32        );
33    }
34
35    if !browsers_dir.exists() {
36        if debug {
37            let _ = writeln!(io::stderr(), "[chrome-search] browsers_dir does not exist");
38        }
39        return None;
40    }
41
42    let entries = match fs::read_dir(&browsers_dir) {
43        Ok(entries) => entries,
44        Err(e) => {
45            let _ = writeln!(
46                io::stderr(),
47                "Warning: cannot read Chrome cache directory {}: {}",
48                browsers_dir.display(),
49                e
50            );
51            return None;
52        }
53    };
54
55    let mut versions: Vec<_> = entries
56        .filter_map(|e| e.ok())
57        .filter(|e| {
58            let matches = e
59                .file_name()
60                .to_str()
61                .is_some_and(|n| n.starts_with("chrome-"));
62            if debug {
63                let _ = writeln!(
64                    io::stderr(),
65                    "[chrome-search] entry {:?} matches={}",
66                    e.file_name(),
67                    matches
68                );
69            }
70            matches
71        })
72        .collect();
73
74    versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
75
76    for entry in versions {
77        let dir = entry.path();
78        if let Some(bin) = chrome_binary_in_dir(&dir) {
79            let exists = bin.exists();
80            if debug {
81                let _ = writeln!(
82                    io::stderr(),
83                    "[chrome-search] candidate {} exists={}",
84                    bin.display(),
85                    exists
86                );
87            }
88            if exists {
89                return Some(bin);
90            }
91        } else if debug {
92            let _ = writeln!(
93                io::stderr(),
94                "[chrome-search] no binary found in {}",
95                dir.display()
96            );
97        }
98    }
99
100    if debug {
101        let _ = writeln!(io::stderr(), "[chrome-search] no installed Chrome found");
102    }
103    None
104}
105
106fn chrome_binary_in_dir(dir: &Path) -> Option<PathBuf> {
107    #[cfg(target_os = "macos")]
108    {
109        let app =
110            dir.join("Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing");
111        if app.exists() {
112            return Some(app);
113        }
114        let inner = dir.join(
115            "chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
116        );
117        if inner.exists() {
118            return Some(inner);
119        }
120        let inner_x64 = dir.join(
121            "chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
122        );
123        if inner_x64.exists() {
124            return Some(inner_x64);
125        }
126        None
127    }
128
129    #[cfg(target_os = "linux")]
130    {
131        let bin = dir.join("chrome");
132        if bin.exists() {
133            return Some(bin);
134        }
135        let inner = dir.join("chrome-linux64/chrome");
136        if inner.exists() {
137            return Some(inner);
138        }
139        None
140    }
141
142    #[cfg(target_os = "windows")]
143    {
144        let bin = dir.join("chrome.exe");
145        if bin.exists() {
146            return Some(bin);
147        }
148        let inner = dir.join("chrome-win64/chrome.exe");
149        if inner.exists() {
150            return Some(inner);
151        }
152        None
153    }
154
155    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
156    {
157        let _ = dir;
158        None
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use std::fs;
166
167    #[test]
168    fn get_browsers_dir_ends_with_product_path() {
169        let dir = get_browsers_dir();
170        let s = dir.to_string_lossy();
171        assert!(
172            s.contains("browser-automation-cli") && s.contains("browsers"),
173            "unexpected browsers dir: {s}"
174        );
175    }
176
177    #[test]
178    fn chrome_binary_in_dir_none_for_empty() {
179        let tmp = tempfile::tempdir().unwrap();
180        assert!(chrome_binary_in_dir(tmp.path()).is_none());
181    }
182
183    #[test]
184    #[cfg(target_os = "linux")]
185    fn chrome_binary_in_dir_finds_linux_layout() {
186        let tmp = tempfile::tempdir().unwrap();
187        let nested = tmp.path().join("chrome-linux64");
188        fs::create_dir_all(&nested).unwrap();
189        let bin = nested.join("chrome");
190        fs::write(&bin, b"x").unwrap();
191        assert_eq!(chrome_binary_in_dir(tmp.path()).as_ref(), Some(&bin));
192    }
193}