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