use std::{
env,
ffi::OsStr,
fs,
io::ErrorKind,
path::{Path, PathBuf},
process::Command,
};
fn main() {
if let Err(error) = build_vmaf() {
panic!("Failed to build VMAF: {error}");
}
}
fn build_vmaf() -> Result<(), Box<dyn std::error::Error>> {
let target = env::var("TARGET")?;
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let target_os = env::var("CARGO_CFG_TARGET_OS")?;
let is_x86 = matches!(target_arch.as_str(), "x86" | "x86_64");
if env::var_os("DOCS_RS").is_some() || target_arch.starts_with("wasm") {
return Ok(());
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let source_dir = manifest_dir.join("vendored/vmaf/libvmaf");
let build_dir = out_dir.join("build");
let install_dir = out_dir.join("install");
if !source_dir.is_dir() {
return Err(format!(
"Missing VMAF source directory: {}. Run 'python vendor_vmaf.py' to download it.",
source_dir.display()
)
.into());
}
println!("cargo:rerun-if-changed=vendored/vmaf");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-env-changed=MESON");
println!("cargo:rerun-if-env-changed=NINJA");
println!("cargo:rerun-if-env-changed=VMAF_MESON_CROSS_FILE");
println!("cargo:rerun-if-env-changed=IPHONEOS_DEPLOYMENT_TARGET");
println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME");
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
println!("cargo:rerun-if-env-changed=ANDROID_NDK_PATH");
println!("cargo:rerun-if-env-changed=NDK_HOME");
println!("cargo:rerun-if-env-changed=ANDROID_PLATFORM");
println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM");
println!("cargo:rerun-if-env-changed=CARGO_NDK_SYSROOT_PATH");
println!("cargo:rerun-if-env-changed=CARGO_NDK_SYSROOT_LIBS_PATH");
let meson = env::var_os("MESON").unwrap_or_else(|| "meson".into());
let ninja = env::var_os("NINJA").unwrap_or_else(|| "ninja".into());
require_build_tool(&meson, "Meson", "MESON")?;
require_build_tool(&ninja, "Ninja", "NINJA")?;
if is_x86 {
require_nasm()?;
}
let asm_enabled = is_x86 || env::var_os("CARGO_FEATURE_ASM").is_some();
let mut setup = Command::new(&meson);
setup
.arg("setup")
.arg(&build_dir)
.arg(&source_dir)
.arg(format!("--prefix={}", install_dir.display()))
.arg("--libdir=lib")
.arg("--default-library=static")
.arg("--buildtype=release")
.arg("-Denable_tests=false")
.arg("-Denable_docs=false")
.arg("-Denable_tools=false")
.arg("-Denable_cuda=false")
.arg("-Denable_nvtx=false")
.arg(feature_option("built-in-models", "built_in_models"))
.arg(format!("-Denable_asm={asm_enabled}"))
.arg(feature_option("float", "enable_float"));
let cross_file = match env::var_os("VMAF_MESON_CROSS_FILE") {
Some(path) => Some(PathBuf::from(path)),
None => generate_mobile_cross_file(&target, &target_arch, &target_os, &out_dir)?,
};
if let Some(cross_file) = cross_file {
setup.arg("--cross-file").arg(cross_file);
}
let setup_status = setup.status()?;
if !setup_status.success() {
return Err("Meson setup failed".into());
}
let compile_status = Command::new(&meson)
.args(["compile", "-C"])
.arg(&build_dir)
.status()?;
if !compile_status.success() {
return Err("Meson compile failed".into());
}
let install_status = Command::new(&meson)
.args(["install", "-C"])
.arg(&build_dir)
.arg("--no-rebuild")
.status()?;
if !install_status.success() {
return Err("Meson install failed".into());
}
println!(
"cargo:rustc-link-search=native={}",
install_dir.join("lib").display()
);
println!("cargo:rustc-link-lib=static=vmaf");
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
match (target_os.as_str(), target_env.as_str()) {
("macos" | "ios", _) => println!("cargo:rustc-link-lib=c++"),
("android", _) => {
if let Some(path) = env::var_os("CARGO_NDK_SYSROOT_LIBS_PATH") {
println!(
"cargo:rustc-link-search=native={}",
PathBuf::from(path).display()
);
}
println!("cargo:rustc-link-lib=c++_shared");
}
("windows", "msvc") => println!("cargo:rustc-link-lib=msvcp140"),
("windows", _) => println!("cargo:rustc-link-lib=stdc++"),
_ => println!("cargo:rustc-link-lib=stdc++"),
}
if target_os == "android" {
println!("cargo:rustc-link-lib=m");
println!("cargo:rustc-link-lib=dl");
} else if matches!(target_os.as_str(), "linux" | "freebsd") {
println!("cargo:rustc-link-lib=m");
println!("cargo:rustc-link-lib=pthread");
}
Ok(())
}
fn feature_option(feature: &str, option: &str) -> String {
let cargo_feature = format!("CARGO_FEATURE_{}", feature.replace('-', "_").to_uppercase());
let value = if env::var_os(cargo_feature).is_some() {
"true"
} else {
"false"
};
format!("-D{option}={value}")
}
fn generate_mobile_cross_file(
target: &str,
target_arch: &str,
target_os: &str,
out_dir: &Path,
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
let contents = match target_os {
"ios" => ios_cross_file(target, target_arch)?,
"android" => android_cross_file(target, target_arch)?,
_ => return Ok(None),
};
let path = out_dir.join(format!("meson-{target}.ini"));
fs::write(&path, contents)?;
Ok(Some(path))
}
fn ios_cross_file(target: &str, target_arch: &str) -> Result<String, Box<dyn std::error::Error>> {
let simulator = target.ends_with("-sim") || target_arch == "x86_64";
let sdk = if simulator {
"iphonesimulator"
} else {
"iphoneos"
};
let deployment_target =
env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "12.0".into());
let clang_arch = match target_arch {
"aarch64" => "arm64",
"x86_64" => "x86_64",
_ => return Err(format!("Unsupported iOS architecture: {target_arch}").into()),
};
let clang_target = format!(
"{clang_arch}-apple-ios{deployment_target}{}",
if simulator { "-simulator" } else { "" }
);
let sdk_path = command_stdout("xcrun", &["--sdk", sdk, "--show-sdk-path"])?;
let clang = command_stdout("xcrun", &["--sdk", sdk, "--find", "clang"])?;
let clangxx = command_stdout("xcrun", &["--sdk", sdk, "--find", "clang++"])?;
let ar = command_stdout("xcrun", &["--sdk", sdk, "--find", "ar"])?;
let strip = command_stdout("xcrun", &["--sdk", sdk, "--find", "strip"])?;
let compiler_args = [
"-target".to_string(),
clang_target,
"-isysroot".to_string(),
sdk_path,
];
Ok(format!(
"[binaries]\nc = {}\ncpp = {}\nar = {}\nstrip = {}\n\n[host_machine]\nsystem = 'darwin'\ncpu_family = {}\ncpu = {}\nendian = 'little'\n\n[properties]\nneeds_exe_wrapper = true\n",
meson_command(&clang, &compiler_args),
meson_command(&clangxx, &compiler_args),
meson_command(&ar, &[]),
meson_command(&strip, &[]),
meson_string(if target_arch == "aarch64" { "aarch64" } else { "x86_64" }),
meson_string(clang_arch),
))
}
fn android_cross_file(
target: &str,
target_arch: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let sysroot = android_sysroot()?;
let prebuilt = sysroot
.parent()
.ok_or("Android NDK sysroot has no parent directory")?;
let bin = prebuilt.join("bin");
let api_level = android_api_level();
let clang_target = match target {
"armv7-linux-androideabi" => "armv7a-linux-androideabi",
_ => target,
};
let target_arg = format!("--target={clang_target}{api_level}");
let sysroot_arg = format!("--sysroot={}", sysroot.display());
let compiler_args = [target_arg, sysroot_arg];
let (cpu_family, cpu) = match target_arch {
"aarch64" => ("aarch64", "aarch64"),
"arm" => ("arm", "armv7"),
"x86_64" => ("x86_64", "x86_64"),
"x86" => ("x86", "i686"),
_ => return Err(format!("Unsupported Android architecture: {target_arch}").into()),
};
Ok(format!(
"[binaries]\nc = {}\ncpp = {}\nar = {}\nstrip = {}\n\n[host_machine]\nsystem = 'android'\ncpu_family = {}\ncpu = {}\nendian = 'little'\n\n[properties]\nneeds_exe_wrapper = true\n",
meson_command(&bin.join("clang").to_string_lossy(), &compiler_args),
meson_command(&bin.join("clang++").to_string_lossy(), &compiler_args),
meson_command(&bin.join("llvm-ar").to_string_lossy(), &[]),
meson_command(&bin.join("llvm-strip").to_string_lossy(), &[]),
meson_string(cpu_family),
meson_string(cpu),
))
}
fn android_sysroot() -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Some(path) = env::var_os("CARGO_NDK_SYSROOT_PATH") {
return Ok(PathBuf::from(path));
}
let ndk_root = [
"ANDROID_NDK_HOME",
"ANDROID_NDK_ROOT",
"ANDROID_NDK_PATH",
"NDK_HOME",
]
.iter()
.find_map(env::var_os)
.map(PathBuf::from)
.ok_or("Android NDK not found. Set ANDROID_NDK_HOME or build through cargo-ndk.")?;
let prebuilt_root = ndk_root.join("toolchains/llvm/prebuilt");
let prebuilt = fs::read_dir(&prebuilt_root)?
.filter_map(Result::ok)
.map(|entry| entry.path())
.find(|path| path.join("bin/clang").is_file())
.ok_or_else(|| {
format!(
"No usable LLVM toolchain found under {}",
prebuilt_root.display()
)
})?;
Ok(prebuilt.join("sysroot"))
}
fn android_api_level() -> String {
["ANDROID_PLATFORM", "CARGO_NDK_PLATFORM"]
.iter()
.find_map(|name| env::var(name).ok())
.map(|value| value.trim_start_matches("android-").to_string())
.filter(|value| !value.is_empty() && value.chars().all(|c| c.is_ascii_digit()))
.unwrap_or_else(|| "21".into())
}
fn command_stdout(program: &str, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new(program).args(args).output()?;
if !output.status.success() {
return Err(format!(
"{} {} failed with status {}",
program,
args.join(" "),
output.status
)
.into());
}
Ok(String::from_utf8(output.stdout)?.trim().to_string())
}
fn meson_command(executable: &str, args: &[String]) -> String {
let values = std::iter::once(executable)
.chain(args.iter().map(String::as_str))
.map(meson_string)
.collect::<Vec<_>>()
.join(", ");
format!("[{values}]")
}
fn meson_string(value: &str) -> String {
format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'"))
}
fn require_build_tool(
executable: &OsStr,
name: &str,
override_variable: &str,
) -> Result<(), Box<dyn std::error::Error>> {
match Command::new(executable).arg("--version").output() {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => Err(format!(
"{name} executable '{}' failed its version check with status {}. Install a working {name} executable or set {override_variable} to its path.",
executable.to_string_lossy(),
output.status
)
.into()),
Err(error) if error.kind() == ErrorKind::NotFound => Err(format!(
"{name} is required to build vmaf-head-sys, but executable '{}' was not found. Install {name} and ensure it is on PATH, or set {override_variable} to its path.",
executable.to_string_lossy()
)
.into()),
Err(error) => Err(format!(
"Failed to run {name} executable '{}': {error}. Install a working {name} executable or set {override_variable} to its path.",
executable.to_string_lossy()
)
.into()),
}
}
fn require_nasm() -> Result<(), Box<dyn std::error::Error>> {
match Command::new("nasm").arg("-v").output() {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => Err(format!(
"NASM failed its version check with status {}. A working NASM installation is required for x86 and x86_64 builds.",
output.status
)
.into()),
Err(error) if error.kind() == ErrorKind::NotFound => Err(
"NASM is required for x86 and x86_64 builds, but `nasm` was not found. Install NASM and ensure it is on PATH."
.into(),
),
Err(error) => Err(format!(
"Failed to run NASM: {error}. Install NASM and ensure `nasm` is on PATH."
)
.into()),
}
}