Fetcher
The fetcher Cargo feature downloads a Chrome binary from Google's
Chrome for Testing (CFT) distribution and hands back a path you
can pass to BrowserBuilder::executable. Useful in CI runners that
don't ship Chrome, in containers, or whenever you want a version pinned
independently of the host's Chrome install.
Enable it in Cargo.toml:
[dependencies]
zendriver = { version = "0.1", features = ["fetcher"] }
Two entry points:
| Entry point | When to use |
|---|---|
BrowserBuilder::ensure_chrome | Common case: just download Chrome and launch. One line, no configuration. |
Fetcher (builder) | Pin a version / channel, customize the cache dir, register progress callbacks. |
The one-liner
For the common "I just want Chrome" path:
#![allow(unused)] fn main() { async fn ex() -> zendriver::Result<()> { let browser = zendriver::Browser::builder() .ensure_chrome().await? .launch().await?; Ok(()) } }
ensure_chrome resolves the latest stable CFT version for the host
platform, downloads + extracts it on cache miss, and points the
[BrowserBuilder] at the resulting binary. On a cache hit the call
returns in milliseconds and skips the network.
The full builder
Fetcher::new returns a builder with sensible defaults. Configure as
needed, then call ensure_chrome:
//! Demonstrates the P5 [`Fetcher`] — Chrome for Testing binary downloader. //! //! [`Fetcher::new`] starts a builder; `.version(VersionSpec::Latest)` //! pins the version selector to the newest Stable build in the CFT //! manifest. `.on_progress(...)` registers a callback that fires through //! every phase ([`FetcherPhase::Resolving`] → `Downloading` → `Extracting` //! → `Verifying` → `Done`). //! //! [`Fetcher::ensure_chrome`] resolves the manifest, downloads + extracts //! into the OS-conventional cache dir on a cache miss, and returns a //! [`PathBuf`] to a runnable Chrome binary. On a cache hit (binary already //! extracted under `<cache>/<version>/`), it skips the network entirely //! and returns the cached path. //! //! After resolving the path, you can hand it to a [`Browser`] launch: //! `Browser::builder().executable(path).launch().await?` — or use the //! one-line shortcut `Browser::builder().ensure_chrome().await?.launch()`, //! which wraps this call internally. //! //! Requires the `fetcher` cargo feature: //! `cargo run --example fetcher_demo --features fetcher`. use zendriver::{Fetcher, VersionSpec}; #[tokio::main] #[allow(clippy::result_large_err)] // example boundary; users wrap in their own Error async fn main() -> zendriver::Result<()> { tracing_subscriber::fmt::init(); let path = Fetcher::new() .version(VersionSpec::Latest) .on_progress(|p| println!("{p:?}")) .ensure_chrome() .await?; println!("chrome binary: {}", path.display()); Ok(()) }
Customization points:
.version(VersionSpec)— pin a release.VersionSpec::Latest— the newest entry in the manifest (default).VersionSpec::Stable— alias forLatesttoday; will diverge if / when CFT exposes a stable-channel JSON.VersionSpec::Channel(Channel::Stable)— onlyStableis fully wired in 0.1;Beta/Dev/CanaryreturnFetcherError::UnsupportedPlatform(the CFT endpoint is separate).VersionSpec::Explicit("126.0.6478.182".into())— exact version string from the manifest.
.platform(Platform)— overridePlatform::auto_detect. Useful for cross-compiling docker images for a different host arch..cache_dir(path)— override the default cache root. Point at a shared CI volume so multiple jobs share one download..on_progress(cb)— receive aFetcherProgresssnapshot on every phase transition + per-chunk during download.
Cache layout
Downloads land in the OS-conventional cache dir under zendriver/chrome:
- Linux —
${XDG_CACHE_HOME:-$HOME/.cache}/zendriver/chrome/ - macOS —
~/Library/Caches/zendriver/chrome/ - Windows —
%LOCALAPPDATA%\zendriver\chrome\
Inside, each version gets its own subdirectory matching the CFT zip layout verbatim:
<cache_dir>/
126.0.6478.182/
chrome-linux64/
chrome (Linux)
chrome-win64/
chrome.exe (Windows)
chrome-mac-arm64/
Google Chrome for Testing.app/Contents/MacOS/... (macOS Apple Silicon)
Writes are atomic. The fetcher downloads + extracts into a
<version>.tmp/ sibling, then a single rename promotes it to
<version>/. Crashing mid-download leaves a .tmp/ that the next run
detects, deletes, and retries — no half-extracted binaries ever appear
under the canonical name.
Progress callbacks
FetcherProgress carries:
phase— one ofResolving/Downloading/Extracting/Verifying/Done.downloaded/total: Option<u64>— bytes for the current phase, withtotalpopulated duringDownloadingfrom theContent-Lengthheader.
The callback runs on Tokio worker threads. Render to a TUI / progress
bar inside it; heavier work (e.g. logging via I/O) should
spawn_blocking itself off the runtime to avoid stalling the download
task.
use indicatif::{ProgressBar, ProgressStyle};
use zendriver::{Fetcher, FetcherPhase};
let bar = ProgressBar::new(0);
let path = Fetcher::new()
.on_progress(move |p| {
if p.phase == FetcherPhase::Downloading {
if let Some(t) = p.total { bar.set_length(t); }
bar.set_position(p.downloaded);
}
})
.ensure_chrome()
.await?;
CI use case
The motivating workflow: GitHub Actions / GitLab / etc runners that don't have Chrome installed. Skipping Chrome from the system image and letting the fetcher download Chrome inside the job has three wins:
- Reproducibility. Pin
VersionSpec::Explicit(...)so the same Chrome runs everywhere. No surprises when the runner image bumps. - Smaller base images. Don't bake Chrome into a hot container image if only a fraction of jobs need it.
- Parallel cache. Point the fetcher at a runner-side volume (CFT binaries are ~150 MB compressed; one download serves every job).
A minimal .github/workflows/test.yml snippet:
- uses: actions/cache@v4
with:
path: ~/.cache/zendriver/chrome
key: zendriver-chrome-${{ runner.os }}-126.0.6478.182
- run: cargo test --features fetcher
actions/cache rehydrates the cache dir; the fetcher detects the cache
hit and skips the download. First run takes ~30 s on GitHub's free
runners; cached runs take <1 s in ensure_chrome.
When NOT to use it
- You already have Chrome on the host and don't care about version-pinning — the built-in PATH discovery is faster.
- Network-restricted environments that can't reach
https://googlechromelabs.github.ioor the CFT CDN — pre-populate the cache out-of-band or ship a Docker image with Chrome baked in. - You need Chrome stable on Linux ARM64 — CFT doesn't ship a
linux-arm64build today;Platform::auto_detectreturnsNoneon that host andensure_chromeerrors out.