branch: main
binary.rs
11940 bytesRaw
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<String>;
}
impl<T: BinaryDep> 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<usize> {
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<PathBuf> {
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::<PathBuf>();
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<String> {
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<String> {
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<String> {
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()),
})
}
}