use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const VERSION: &str = "26.6.0";
const STATIC_LIBS: &[&str] = &["webp", "sharpyuv"];
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=WEBP_BINARIES_DIR");
let binaries_dir = locate_binaries();
let lib_dir = binaries_dir.join("lib");
let include_dir = binaries_dir.join("include");
emit_link_directives(&lib_dir);
generate_bindings(&include_dir);
}
fn locate_binaries() -> PathBuf {
if let Ok(dir) = env::var("WEBP_BINARIES_DIR") {
let dir = PathBuf::from(dir);
assert!(
dir.join("include").is_dir() && dir.join("lib").is_dir(),
"WEBP_BINARIES_DIR ({}) must contain `include/` and `lib/` subdirectories",
dir.display()
);
return dir;
}
download_and_extract()
}
fn download_and_extract() -> PathBuf {
let archive = archive_name();
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let cache_dir = out_dir.join(format!("binaries-webp-{VERSION}"));
if cache_dir.join("lib").is_dir() && cache_dir.join("include").is_dir() {
return cache_dir;
}
let url = format!("https://github.com/vegidio/binaries-webp/releases/download/{VERSION}/{archive}");
eprintln!("webp-rs: downloading {url}");
let bytes = download(&url);
extract_zip(&bytes, &cache_dir);
cache_dir
}
fn archive_name() -> String {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let os = match target_os.as_str() {
"linux" => "linux",
"macos" => "osx",
"windows" => "windows",
other => panic!("webp-rs: unsupported target OS `{other}`"),
};
let arch = match target_arch.as_str() {
"x86_64" => "x64",
"aarch64" => "arm64",
other => panic!("webp-rs: unsupported target architecture `{other}`"),
};
format!("static_{os}_{arch}.zip")
}
fn download(url: &str) -> Vec<u8> {
let mut reader = ureq::get(url)
.call()
.unwrap_or_else(|e| panic!("webp-rs: failed to download {url}: {e}"))
.into_body()
.into_reader();
let mut bytes = Vec::new();
io::copy(&mut reader, &mut bytes)
.unwrap_or_else(|e| panic!("webp-rs: failed to read response body from {url}: {e}"));
bytes
}
fn extract_zip(bytes: &[u8], dest: &Path) {
let reader = io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(reader).expect("webp-rs: invalid zip archive");
for i in 0..zip.len() {
let mut entry = zip.by_index(i).expect("webp-rs: corrupt zip entry");
let Some(rel_path) = entry.enclosed_name() else {
continue; };
let out_path = dest.join(rel_path);
if entry.is_dir() {
fs::create_dir_all(&out_path).unwrap();
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent).unwrap();
}
let mut out_file = fs::File::create(&out_path)
.unwrap_or_else(|e| panic!("webp-rs: cannot create {}: {e}", out_path.display()));
io::copy(&mut entry, &mut out_file).expect("webp-rs: failed to extract file");
}
}
fn emit_link_directives(lib_dir: &Path) {
println!("cargo:rustc-link-search=native={}", lib_dir.display());
for lib in STATIC_LIBS {
println!("cargo:rustc-link-lib=static={lib}");
}
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
match target_os.as_str() {
"linux" => {
println!("cargo:rustc-link-lib=dylib=m");
println!("cargo:rustc-link-lib=dylib=pthread");
}
"windows" => {
println!("cargo:rustc-link-lib=dylib=pthread");
}
_ => {}
}
}
fn generate_bindings(include_dir: &Path) {
let encode_h = include_dir.join("encode.h");
let decode_h = include_dir.join("decode.h");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let bindings = bindgen::Builder::default()
.header(encode_h.to_string_lossy())
.header(decode_h.to_string_lossy())
.clang_arg(format!("-I{}", include_dir.display()))
.allowlist_function("WebP.*")
.allowlist_function("VP8.*")
.allowlist_type("WebP.*")
.allowlist_type("VP8.*")
.allowlist_var("WEBP.*")
.generate_comments(false)
.layout_tests(false)
.generate()
.expect("webp-rs: failed to generate bindings from encode.h/decode.h");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("webp-rs: failed to write bindings.rs");
}