use std::path::{Path, PathBuf};
use crate::browser_fetch;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Chrome,
Chromium,
Edge,
Firefox,
Servo,
Auto,
}
impl Backend {
pub fn from_env() -> Self {
match std::env::var("SHIRABE_BACKEND")
.ok()
.map(|s| s.trim().to_ascii_lowercase())
.as_deref()
{
Some("chrome") => Backend::Chrome,
Some("chromium") => Backend::Chromium,
Some("edge") => Backend::Edge,
Some("firefox") => Backend::Firefox,
Some("servo") => Backend::Servo,
_ => Backend::Auto,
}
}
pub fn order(self) -> &'static [Backend] {
const AUTO: &[Backend] = &[Backend::Chrome, Backend::Chromium, Backend::Edge];
const CHROME: &[Backend] = &[Backend::Chrome];
const CHROMIUM: &[Backend] = &[Backend::Chromium];
const EDGE: &[Backend] = &[Backend::Edge];
const FIREFOX: &[Backend] = &[Backend::Firefox];
const SERVO: &[Backend] = &[Backend::Servo];
match self {
Backend::Auto => AUTO,
Backend::Chrome => CHROME,
Backend::Chromium => CHROMIUM,
Backend::Edge => EDGE,
Backend::Firefox => FIREFOX,
Backend::Servo => SERVO,
}
}
pub fn label(self) -> &'static str {
match self {
Backend::Chrome => "chrome",
Backend::Chromium => "chromium",
Backend::Edge => "edge",
Backend::Firefox => "firefox",
Backend::Servo => "servo",
Backend::Auto => "auto",
}
}
pub fn engine_id(self) -> Option<&'static str> {
match self {
Backend::Firefox => Some("firefox"),
Backend::Servo => Some("servo"),
_ => None,
}
}
pub fn is_cdp(self) -> bool {
matches!(self, Backend::Chrome | Backend::Chromium | Backend::Edge)
}
fn candidates(self) -> Vec<PathBuf> {
if !self.is_cdp() {
return Vec::new();
}
system_candidates(self)
}
fn env_overrides(self) -> &'static [&'static str] {
match self {
Backend::Chrome => &["CHROME_PATH"],
Backend::Chromium => &["CHROMIUM_PATH"],
Backend::Edge => &["EDGE_PATH"],
Backend::Firefox | Backend::Servo | Backend::Auto => &[],
}
}
}
pub fn resolve() -> anyhow::Result<(Backend, PathBuf)> {
let selected = Backend::from_env();
if let Some(id) = selected.engine_id() {
return Err(anyhow::anyhow!(
"backend `{}` is driven through the FFI engine contract, not as a \
spawned executable. Open it via shirabe::ffi::CdylibEngine::open; \
the vendor lib `libshirabe_engine_{id}` is resolved separately.",
selected.label()
));
}
for backend in selected.order() {
for var in backend.env_overrides() {
if let Ok(p) = std::env::var(var) {
if !p.is_empty() {
let path = PathBuf::from(&p);
if path.exists() {
return Ok((*backend, path));
}
anyhow::bail!(
"{var} is set to {p:?} but it does not exist",
var = var,
p = p
);
}
}
}
if *backend == Backend::Chrome {
if let Some(p) = option_env!("SHIRABE_BROWSER_PATH") {
if !p.is_empty() && Path::new(p).exists() {
return Ok((Backend::Chrome, PathBuf::from(p)));
}
}
}
if let Some(p) = backend.candidates().into_iter().next() {
return Ok((*backend, p));
}
}
match browser_fetch::resolve() {
Ok(path) => Ok((Backend::Chrome, path)),
Err(e) => Err(e),
}
}
pub fn resolve_executable() -> Result<String, String> {
resolve()
.map(|(_, p)| p.to_string_lossy().to_string())
.map_err(|e| e.to_string())
}
fn system_candidates(backend: Backend) -> Vec<PathBuf> {
let names: &[&str] = match backend {
Backend::Chrome => &[
"google-chrome",
"google-chrome-stable",
"chrome",
"chromium-browser",
"chromium",
],
Backend::Chromium => &["chromium", "chromium-browser"],
Backend::Edge => &["microsoft-edge", "microsoft-edge-stable"],
Backend::Firefox | Backend::Servo => &[],
Backend::Auto => &[
"google-chrome",
"google-chrome-stable",
"chromium-browser",
"chromium",
"microsoft-edge",
],
};
let mut hits = Vec::new();
if let Some(path_var) = std::env::var_os("PATH") {
let try_names: Vec<String> = if cfg!(windows) {
let mut v: Vec<String> = names.iter().map(|s| format!("{s}.exe")).collect();
v.extend(names.iter().map(|s| s.to_string()));
v
} else {
names.iter().map(|s| s.to_string()).collect()
};
for dir in std::env::split_paths(&path_var) {
for name in &try_names {
let candidate = dir.join(name);
if is_executable_file(&candidate) {
hits.push(candidate);
}
}
}
}
for p in well_known_locations(backend) {
let candidate = PathBuf::from(p);
if is_executable_file(&candidate) {
hits.push(candidate);
}
}
hits
}
fn well_known_locations(backend: Backend) -> &'static [&'static str] {
match (cfg!(target_os = "macos"), cfg!(target_os = "windows")) {
(true, _) => match backend {
Backend::Chromium => &["/Applications/Chromium.app/Contents/MacOS/Chromium"],
Backend::Edge => &["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
_ => &["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
},
(false, true) => match backend {
Backend::Chromium => &[r"C:\Program Files\Chromium\Application\chrome.exe"],
Backend::Edge => &[r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"],
_ => &[
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
],
},
_ => match backend {
Backend::Chromium => &[
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
],
Backend::Edge => &["/usr/bin/microsoft-edge", "/usr/bin/microsoft-edge-stable"],
_ => &[
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/snap/bin/chromium",
],
},
}
}
fn is_executable_file(path: &Path) -> bool {
std::fs::metadata(path)
.map(|m| m.is_file())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_expands_to_preference_order() {
assert_eq!(
Backend::Auto.order(),
&[Backend::Chrome, Backend::Chromium, Backend::Edge]
);
assert_eq!(Backend::Edge.order(), &[Backend::Edge]);
}
#[test]
fn env_overrides_match_backend() {
assert_eq!(Backend::Chrome.env_overrides(), &["CHROME_PATH"]);
assert_eq!(Backend::Chromium.env_overrides(), &["CHROMIUM_PATH"]);
assert_eq!(Backend::Edge.env_overrides(), &["EDGE_PATH"]);
assert!(Backend::Auto.env_overrides().is_empty());
}
#[test]
#[serial_test::serial]
fn from_env_parses_known_values() {
let restore = std::env::var_os("SHIRABE_BACKEND");
for (raw, expected) in [
("chrome", Backend::Chrome),
(" Chromium ", Backend::Chromium),
("EDGE", Backend::Edge),
("firefox", Backend::Firefox),
("SERVO", Backend::Servo),
("unknown", Backend::Auto),
] {
unsafe { std::env::set_var("SHIRABE_BACKEND", raw) };
assert_eq!(Backend::from_env(), expected, "raw = {raw:?}");
}
unsafe {
match &restore {
Some(v) => std::env::set_var("SHIRABE_BACKEND", v),
None => std::env::remove_var("SHIRABE_BACKEND"),
}
}
}
#[test]
#[serial_test::serial]
fn foreign_backends_defer_to_ffi_layer() {
let restore = std::env::var_os("SHIRABE_BACKEND");
unsafe { std::env::set_var("SHIRABE_BACKEND", "firefox") };
let err = resolve().unwrap_err().to_string();
assert!(err.contains("FFI"), "unexpected error: {err}");
assert_eq!(Backend::Firefox.engine_id(), Some("firefox"));
assert_eq!(Backend::Servo.engine_id(), Some("servo"));
assert!(Backend::Chrome.engine_id().is_none());
assert!(!Backend::Firefox.is_cdp());
unsafe {
match restore {
Some(v) => std::env::set_var("SHIRABE_BACKEND", v),
None => std::env::remove_var("SHIRABE_BACKEND"),
}
}
}
}