use std::path::{Path, PathBuf};
use tracing::{debug, info};
use a3s_use_core::{UseError, UseResult};
use crate::pool::browser_error;
#[cfg(target_os = "macos")]
const KNOWN_PATHS: &[&str] = &[
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
];
#[cfg(all(unix, not(target_os = "macos")))]
const KNOWN_PATHS: &[&str] = &[
"/opt/google/chrome/chrome",
"/opt/chromium.org/chromium/chrome",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
];
#[cfg(target_os = "windows")]
const KNOWN_PATHS: &[&str] = &[
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
];
const KNOWN_COMMANDS: &[&str] = &[
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"chrome",
];
pub(crate) fn platform_id() -> UseResult<&'static str> {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
Ok("mac-arm64")
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
{
Ok("mac-x64")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
Ok("linux64")
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
{
Ok("win64")
}
#[cfg(all(target_os = "windows", target_arch = "x86"))]
{
Ok("win32")
}
#[cfg(not(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "windows", target_arch = "x86"),
)))]
{
Err(browser_error(
"Unsupported platform for automatic Chrome download".to_string(),
))
}
}
#[cfg(target_os = "macos")]
pub(crate) fn chrome_executable_in_zip(platform: &str) -> String {
format!(
"chrome-{}/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
platform
)
}
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) fn chrome_executable_in_zip(platform: &str) -> String {
format!("chrome-{}/chrome", platform)
}
#[cfg(target_os = "windows")]
pub(crate) fn chrome_executable_in_zip(platform: &str) -> String {
format!("chrome-{}\\chrome.exe", platform)
}
pub(crate) fn managed_cache_dir() -> UseResult<PathBuf> {
Ok(browser_data_root()?.join("chrome"))
}
pub(crate) fn browser_data_root() -> UseResult<PathBuf> {
if let Some(value) = std::env::var_os("A3S_USE_BROWSER_HOME") {
return absolute(PathBuf::from(value));
}
if let Some(value) = std::env::var_os("A3S_DATA_HOME") {
return Ok(absolute(PathBuf::from(value))?.join("use/browser"));
}
if let Some(value) = std::env::var_os("XDG_DATA_HOME") {
return Ok(absolute(PathBuf::from(value))?.join("a3s/use/browser"));
}
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
return Ok(absolute(home)?.join(".local/share/a3s/use/browser"));
}
#[cfg(windows)]
if let Some(value) = std::env::var_os("LOCALAPPDATA") {
return Ok(absolute(PathBuf::from(value))?.join("a3s/use/browser"));
}
Err(browser_error(
"Cannot determine the A3S Use Browser data directory.",
))
}
fn absolute(path: PathBuf) -> UseResult<PathBuf> {
if path.is_absolute() {
return Ok(path);
}
std::env::current_dir()
.map(|current| current.join(path))
.map_err(|error| browser_error(format!("Failed to resolve browser data path: {error}")))
}
pub fn detect_chrome() -> Option<PathBuf> {
for name in ["A3S_BROWSER_EXECUTABLE", "CHROME"] {
if let Some(path) = std::env::var_os(name).map(PathBuf::from) {
if path.is_file() {
debug!("Chrome found via {name}: {}", path.display());
return Some(path);
}
}
}
for cmd in KNOWN_COMMANDS {
if let Ok(path) = which::which(cmd) {
debug!("Chrome found in PATH: {}", path.display());
return Some(path);
}
}
for path_str in KNOWN_PATHS {
let p = Path::new(path_str);
if p.exists() {
debug!("Chrome found at known path: {}", path_str);
return Some(p.to_path_buf());
}
}
None
}
pub async fn ensure_chrome() -> UseResult<PathBuf> {
if let Some(path) = detect_chrome() {
info!("Using system Chrome: {}", path.display());
return Ok(path);
}
if let Ok(path) = find_managed_chrome() {
info!("Using cached Chrome: {}", path.display());
return Ok(path);
}
info!("No Chrome installation found, downloading Chrome for Testing...");
download_latest_chrome().await
}
pub(crate) fn resolve_chrome() -> UseResult<PathBuf> {
detect_chrome()
.or_else(|| find_managed_chrome().ok())
.ok_or_else(|| {
UseError::new(
"use.browser.runtime_missing",
"No compatible Chrome executable is installed.",
)
.with_suggestion(
"Run 'a3s install use/browser' or select BrowserProvider::ManagedChrome.",
)
})
}
pub(crate) fn find_managed_chrome() -> UseResult<PathBuf> {
let base = managed_cache_dir()?;
if !base.exists() {
return Err(browser_error("No cached Chrome found".to_string()));
}
let mut versions: Vec<_> = std::fs::read_dir(&base)
.map_err(|e| browser_error(format!("Failed to read cache dir: {}", e)))?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.path().is_dir() && !entry.file_name().to_string_lossy().starts_with('.')
})
.collect();
versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
let platform = platform_id()?;
for version_dir in versions {
let Some(version) = version_dir.file_name().to_str().map(str::to_owned) else {
continue;
};
if !crate::install::has_complete_receipt(&version_dir.path(), "chrome", &version) {
continue;
}
let exe_path = version_dir.path().join(chrome_executable_in_zip(platform));
if exe_path.is_file() {
return Ok(exe_path);
}
}
Err(browser_error("No cached Chrome found".to_string()))
}
pub async fn download_latest_chrome() -> UseResult<PathBuf> {
crate::chrome_install::download_latest_chrome().await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_browser_candidates_are_defined() {
assert!(!KNOWN_PATHS.is_empty());
assert!(!KNOWN_COMMANDS.is_empty());
}
}