use clap::{Parser, Subcommand};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
const DRIVER_BASE_URL: &str = "https://playwright.azureedge.net/builds/driver";
#[derive(Parser)]
#[command(name = "playwright-rs", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Install {
browsers: Vec<String>,
#[arg(long)]
with_deps: bool,
#[arg(long)]
driver_only: bool,
},
}
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
match cli.cmd {
Cmd::Install {
browsers,
with_deps,
driver_only,
} => match run_install(browsers, with_deps, driver_only).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("playwright-rs: {e}");
ExitCode::FAILURE
}
},
}
}
async fn run_install(
browsers: Vec<String>,
with_deps: bool,
driver_only: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let version = env!("PLAYWRIGHT_DRIVER_VERSION");
let platform = env!("PLAYWRIGHT_DRIVER_PLATFORM");
let driver_dir = ensure_driver_in_user_cache(version, platform)?;
eprintln!("Driver ready at: {}", driver_dir.display());
if driver_only {
return Ok(());
}
unsafe {
std::env::set_var("PLAYWRIGHT_DRIVER_PATH", &driver_dir);
}
let browser_refs: Vec<&str> = browsers.iter().map(String::as_str).collect();
let browsers_arg: Option<&[&str]> = if browser_refs.is_empty() {
None
} else {
Some(&browser_refs)
};
if with_deps {
playwright_rs::install_browsers_with_deps(browsers_arg).await?;
} else {
playwright_rs::install_browsers(browsers_arg).await?;
}
Ok(())
}
fn ensure_driver_in_user_cache(
version: &str,
platform: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let cache_root = dirs::cache_dir().ok_or("could not determine user cache directory")?;
let driver_dir = cache_root
.join("playwright-rust")
.join(version)
.join(format!("playwright-{version}-{platform}"));
let cli_js = driver_dir.join("package").join("cli.js");
if cli_js.exists() {
return Ok(driver_dir);
}
let parent = driver_dir.parent().expect("driver_dir always has a parent");
fs::create_dir_all(parent)?;
let url = format!("{DRIVER_BASE_URL}/playwright-{version}-{platform}.zip");
eprintln!("Downloading driver from {url}");
let mut response = ureq::get(&url).call()?;
let status = response.status().as_u16();
if !(200..300).contains(&status) {
return Err(format!("download failed with status: {status}").into());
}
let bytes: Vec<u8> = response
.body_mut()
.with_config()
.limit(u64::MAX)
.read_to_vec()?;
eprintln!("Downloaded {} bytes", bytes.len());
let cursor = io::Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(cursor)?;
extract_zip_to(&mut archive, &driver_dir)?;
Ok(driver_dir)
}
fn extract_zip_to(
archive: &mut zip::ZipArchive<io::Cursor<Vec<u8>>>,
dest: &Path,
) -> io::Result<()> {
fs::create_dir_all(dest)?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| io::Error::other(format!("zip read failed: {e}")))?;
let outpath = dest.join(file.name());
if file.is_dir() {
fs::create_dir_all(&outpath)?;
} else {
if let Some(parent) = outpath.parent() {
fs::create_dir_all(parent)?;
}
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if outpath.ends_with("node")
|| outpath.extension().and_then(|s| s.to_str()) == Some("sh")
{
let mut perms = fs::metadata(&outpath)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&outpath, perms)?;
}
}
}
}
Ok(())
}