use crate::build::PBAR; use crate::emoji::{CONFIG, DOWN_ARROW}; use crate::versions::{CUR_ESBUILD_VERSION, CUR_WASM_OPT_VERSION}; use anyhow::{bail, Context, Result}; use flate2::read::GzDecoder; use heck::ToShoutySnakeCase; use std::env; use std::{ fs::{create_dir_all, read_dir, OpenOptions}, path::{Path, PathBuf}, }; pub trait GetBinary: BinaryDep { /// Get the given binary path for a binary dependency /// Returns both the path and a boolean indicating if user provided an override fn get_binary(&self, bin_name: Option<&str>) -> Result<(PathBuf, bool)> { let full_name = self.full_name(); let name = self.name(); let target = self.target(); let version = self.version(); // 1. First check {BIN_NAME}_BIN env override let check_env_var = bin_name.unwrap_or(name).to_shouty_snake_case() + "_BIN"; if let Ok(custom_bin) = env::var(&check_env_var) { match which::which(&custom_bin) { Ok(resolved) => { PBAR.info(&format!( "{CONFIG}Using custom {full_name} from {check_env_var}: {}", resolved.display() )); return Ok((resolved, true)); } Err(_) => { PBAR.warn(&format!("{check_env_var}={custom_bin} not found, falling back to internal {full_name} implementation")); } } } // 2. Then check the cache path let cache_path = cache_path(name, &version, target)?; let bin_path = cache_path.join(self.bin_path(bin_name)?); if bin_path.exists() { return Ok((bin_path, false)); } // 3. Finally perform a download, clearing cache for this name and target first let url = self.download_url(); let _ = remove_all_versions(name, target); PBAR.info(&format!("{DOWN_ARROW}Downloading {full_name}@{version}...")); download(&url, &cache_path)?; if !bin_path.exists() { bail!( "Unable to locate binary {} in {full_name}", bin_path.to_string_lossy() ); } Ok((bin_path, false)) } } pub trait BinaryDep: Sized { /// Returns the name of the binary fn name(&self) -> &'static str; /// Returns the full name of the binary fn full_name(&self) -> &'static str; /// Returns the target of the binary fn target(&self) -> &'static str; /// Returns the latest current version of the binary fn version(&self) -> String; /// Returns the URL for the binary to be downloaded /// as well as the path string within the archive to use fn download_url(&self) -> String; /// Get the relative path of the given binary in the package /// If None, returns the default binary fn bin_path(&self, name: Option<&str>) -> Result; } impl GetBinary for T {} const MAYBE_EXE: &str = if cfg!(windows) { ".exe" } else { "" }; /// For clearing the cache, remove all files for the given binary and target fn remove_all_versions(name: &str, target: &str) -> Result { let prefix_name = format!("{name}-{target}-"); let dir = dirs_next::cache_dir() .unwrap_or_else(std::env::temp_dir) .join("worker-build"); let mut deleted_count = 0; for entry in read_dir(&dir) .with_context(|| format!("Failed to read cache directory {}", dir.display()))? { let entry = entry?; let file_name = entry.file_name(); if let Some(name_str) = file_name.to_str() { if name_str.starts_with(&prefix_name) { let path = entry.path(); if path.is_dir() { std::fs::remove_dir_all(&path).with_context(|| { format!("Failed to remove cached directory {}", path.display()) })?; } else { std::fs::remove_file(&path).with_context(|| { format!("Failed to remove cached file {}", path.display()) })?; } deleted_count += 1; } } } Ok(deleted_count) } /// Cache path for this binary instance fn cache_path(name: &str, version: &str, target: &str) -> Result { let path_name = format!("{name}-{target}-{version}"); let path = dirs_next::cache_dir() .unwrap_or_else(std::env::temp_dir) .join("worker-build") .join(&path_name); if !path.exists() { create_dir_all(&path) .with_context(|| format!("Failed to create cache directory {}", path.display()))?; } Ok(path) } #[cfg(target_family = "unix")] fn fix_permissions(options: &mut OpenOptions) -> &mut OpenOptions { use std::os::unix::fs::OpenOptionsExt; options.mode(0o755) } #[cfg(target_family = "windows")] fn fix_permissions(options: &mut OpenOptions) -> &mut OpenOptions { options } /// Download this binary instance into its cache path fn download(url: &str, bin_dir: &Path) -> Result<()> { let agent = ureq::Agent::config_builder() .tls_config( ureq::tls::TlsConfig::builder() .provider(ureq::tls::TlsProvider::NativeTls) .root_certs(ureq::tls::RootCerts::PlatformVerifier) .build(), ) .build() .new_agent(); let mut res = agent .get(url) .call() .with_context(|| format!("Failed to fetch URL {url}"))?; let body = res.body_mut().as_reader(); let deflater = GzDecoder::new(body); let mut archive = tar::Archive::new(deflater); for entry in archive .entries() .context("Failed to read archive entries")? { let mut entry = entry?; let path_stripped = entry.path()?.components().skip(1).collect::(); let bin_path = bin_dir.join(path_stripped); if entry.header().entry_type().is_dir() { std::fs::create_dir_all(&bin_path) .with_context(|| format!("Failed to create directory {}", bin_path.display()))?; } else { if let Some(parent) = bin_path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("Failed to create directory {}", parent.display()))?; } let mut options = std::fs::OpenOptions::new(); let options = fix_permissions(&mut options); let mut file = options .create(true) .write(true) .open(&bin_path) .with_context(|| format!("Failed to create file {}", bin_path.display()))?; std::io::copy(&mut entry, &mut file) .with_context(|| format!("Failed to extract file {}", bin_path.display()))?; } } Ok(()) } pub struct Esbuild; impl BinaryDep for Esbuild { fn full_name(&self) -> &'static str { "Esbuild" } fn name(&self) -> &'static str { "esbuild" } fn version(&self) -> String { CUR_ESBUILD_VERSION.to_string() } fn target(&self) -> &'static str { match (std::env::consts::OS, std::env::consts::ARCH) { ("android", "arm") => "android-arm", ("android", "aarch64") => "android-arm64", ("android", "x86_64") => "android-x64", ("macos", "aarch64") => "darwin-arm64", ("macos", "x86_64") => "darwin-x64", ("freebsd", "aarch64") => "freebsd-arm64", ("freebsd", "x86_64") => "freebsd-x64", ("linux", "arm") => "linux-arm", ("linux", "aarch64") => "linux-arm64", ("linux", "x86") => "linux-ia32", ("linux", "powerpc64") => "linux-ppc64", ("linux", "s390x") => "linux-s390x", ("linux", "x86_64") => "linux-x64", ("netbsd", "aarch64") => "netbsd-arm64", ("netbsd", "x86_64") => "netbsd-x64", ("openbsd", "aarch64") => "openbsd-arm64", ("openbsd", "x86_64") => "openbsd-x64", ("solaris", "x86_64") => "sunos-x64", ("windows", "aarch64") => "win32-arm64", ("windows", "x86") => "win32-ia32", ("windows", "x86_64") => "win32-x64", _ => panic!("Platform unsupported by esbuild."), } } fn download_url(&self) -> String { let version = self.version(); let target = self.target(); format!("https://registry.npmjs.org/@esbuild/{target}/-/{target}-{version}.tgz") } fn bin_path(&self, name: Option<&str>) -> Result { Ok(match name { None | Some("esbuild") => { if cfg!(windows) { format!("esbuild{MAYBE_EXE}") } else { format!("bin/esbuild{MAYBE_EXE}") } } Some(name) => bail!("Unknown binary {name} in {}", self.full_name()), }) } } pub struct WasmOpt; impl BinaryDep for WasmOpt { fn full_name(&self) -> &'static str { "Wasm Opt" } fn name(&self) -> &'static str { "wasm-opt" } fn version(&self) -> String { CUR_WASM_OPT_VERSION.to_owned() } fn target(&self) -> &'static str { match (std::env::consts::OS, std::env::consts::ARCH) { ("macos", "aarch64") => "arm64-macos", ("macos", "x86_64") => "x86_64-macos", ("linux" | "freebsd" | "netbsd" | "openbsd" | "android", "aarch64") => "aarch64-linux", ("linux" | "freebsd" | "netbsd" | "openbsd", "x86_64") => "x86_64-linux", ("windows", "aarch64") => "arm64-windows", ("windows", "x86_64") => "x86_64-windows", _ => panic!("Platform unsupported for {}", self.full_name()), } } fn download_url(&self) -> String { let version = self.version(); let target = self.target(); format!("https://github.com/WebAssembly/binaryen/releases/download/version_{version}/binaryen-version_{version}-{target}.tar.gz") } fn bin_path(&self, name: Option<&str>) -> Result { Ok(match name { None | Some("wasm-opt") => format!("bin/wasm-opt{MAYBE_EXE}"), Some(name) => bail!("Unknown binary {name} in {}", self.full_name()), }) } } pub struct WasmBindgen<'a>(pub &'a str); impl BinaryDep for WasmBindgen<'_> { fn full_name(&self) -> &'static str { "Wasm Bindgen" } fn name(&self) -> &'static str { "wasm-bindgen" } fn version(&self) -> String { self.0.to_owned() } fn target(&self) -> &'static str { match (std::env::consts::OS, std::env::consts::ARCH) { ("macos", "aarch64") => "aarch64-apple-darwin", ("macos", "x86_64") => "x86_64-apple-darwin", ("linux" | "freebsd" | "netbsd" | "openbsd" | "android", "aarch64") => { "aarch64-unknown-linux-musl" } ("linux" | "freebsd" | "netbsd" | "openbsd", "x86_64") => "x86_64-unknown-linux-musl", ("windows", "x86_64" | "aarch64") => "x86_64-pc-windows-msvc", _ => panic!("Platform unsupported for {}", self.full_name()), } } fn download_url(&self) -> String { let version = self.version(); let target = self.target(); format!("https://github.com/wasm-bindgen/wasm-bindgen/releases/download/{version}/wasm-bindgen-{version}-{target}.tar.gz") } fn bin_path(&self, name: Option<&str>) -> Result { Ok(match name { None | Some("wasm-bindgen") => format!("wasm-bindgen{MAYBE_EXE}"), Some("wasm-bindgen-test-runner") => format!("wasm-bindgen-test-runner{MAYBE_EXE}"), Some(name) => bail!("Unknown binary {name} in {}", self.full_name()), }) } }