#![allow(clippy::panic)]
#![allow(clippy::expect_used)]
use std::env;
use std::path::{Path, PathBuf};
fn main() {
if env::var("DOCS_RS").is_ok() {
let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
std::fs::write(out_path.join("bindings.rs"), b"")
.expect("Couldn't write stub bindings for docs.rs");
println!("cargo:rustc-cfg=docsrs");
return;
}
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let include_paths = match target_os.as_str() {
"windows" => configure_windows(),
"macos" => configure_macos(),
"linux" => configure_linux(),
other => panic!("Unsupported platform: {other}"),
};
emit_api_cfg_flags(&include_paths);
generate_bindings(&include_paths);
}
const FFMPEG_LIBS: &[&str] = &[
"avformat",
"avcodec",
"avutil",
"swscale",
"swresample",
"avfilter",
];
fn configure_windows() -> Vec<String> {
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
println!("cargo:rerun-if-env-changed=LIBCLANG_PATH");
let vcpkg_root = env::var("VCPKG_ROOT").unwrap_or_else(|_| "C:\\vcpkg".to_string());
let installed_path = Path::new(&vcpkg_root).join("installed").join("x64-windows");
let lib_path = installed_path.join("lib");
let include_path = installed_path.join("include");
let bin_path = installed_path.join("bin");
if !lib_path.exists() {
panic!(
"VCPKG FFmpeg not found at: {}\n\
Please install FFmpeg via VCPKG:\n\
vcpkg install ffmpeg:x64-windows",
lib_path.display()
);
}
for lib in FFMPEG_LIBS {
let lib_file = lib_path.join(format!("{lib}.lib"));
if !lib_file.exists() {
panic!(
"FFmpeg library not found: {}\n\
Please reinstall FFmpeg via VCPKG:\n\
vcpkg install ffmpeg:x64-windows",
lib_file.display()
);
}
}
println!("cargo:rustc-link-search=native={}", lib_path.display());
for lib in FFMPEG_LIBS {
println!("cargo:rustc-link-lib=dylib={lib}");
}
if bin_path.exists() {
println!("cargo:rustc-env=FFMPEG_DLL_PATH={}", bin_path.display());
}
configure_llvm_for_bindgen();
vec![include_path.to_string_lossy().into_owned()]
}
fn configure_llvm_for_bindgen() {
let llvm_paths = [
env::var("LIBCLANG_PATH").ok(),
Some("C:\\Program Files\\LLVM\\bin".to_string()),
Some("C:\\Program Files (x86)\\LLVM\\bin".to_string()),
env::var("LLVM_HOME").ok().map(|p| format!("{p}\\bin")),
];
for path in llvm_paths.into_iter().flatten() {
let clang_dll = Path::new(&path).join("libclang.dll");
if clang_dll.exists() {
unsafe {
env::set_var("LIBCLANG_PATH", &path);
}
return;
}
}
if env::var("LIBCLANG_PATH").is_ok() {
return;
}
println!(
"cargo:warning=LLVM/clang not found. Set LIBCLANG_PATH environment variable \
to the LLVM bin directory containing libclang.dll"
);
}
fn configure_macos() -> Vec<String> {
println!("cargo:rerun-if-env-changed=HOMEBREW_PREFIX");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
if let Some(paths) = try_homebrew() {
return paths;
}
if let Some(paths) = try_pkgconfig_unix() {
return paths;
}
panic!(
"FFmpeg not found on macOS.\n\
Please install FFmpeg via Homebrew:\n\
brew install ffmpeg\n\n\
Or ensure pkg-config can find FFmpeg:\n\
export PKG_CONFIG_PATH=\"/path/to/ffmpeg/lib/pkgconfig\""
);
}
fn try_homebrew() -> Option<Vec<String>> {
let homebrew_prefix = env::var("HOMEBREW_PREFIX").unwrap_or_else(|_| {
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if arch == "aarch64" {
"/opt/homebrew".to_string()
} else {
"/usr/local".to_string()
}
});
let homebrew_path = Path::new(&homebrew_prefix);
let lib_path = homebrew_path.join("lib");
let include_path = homebrew_path.join("include");
if !lib_path.exists() {
return None;
}
let mut all_found = true;
for lib in FFMPEG_LIBS {
let dylib_file = lib_path.join(format!("lib{lib}.dylib"));
if !dylib_file.exists() {
let static_file = lib_path.join(format!("lib{lib}.a"));
if !static_file.exists() {
all_found = false;
break;
}
}
}
if !all_found {
return None;
}
let avcodec_header = include_path.join("libavcodec").join("avcodec.h");
if !avcodec_header.exists() {
return None;
}
println!("cargo:rustc-link-search=native={}", lib_path.display());
for lib in FFMPEG_LIBS {
println!("cargo:rustc-link-lib=dylib={lib}");
}
Some(vec![include_path.to_string_lossy().into_owned()])
}
fn configure_linux() -> Vec<String> {
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
if let Some(paths) = try_pkgconfig_unix() {
return paths;
}
panic!(
"FFmpeg not found on Linux.\n\
Please install FFmpeg development packages:\n\n\
Ubuntu/Debian:\n\
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev\n\n\
Fedora:\n\
sudo dnf install ffmpeg-devel\n\n\
Arch Linux:\n\
sudo pacman -S ffmpeg\n\n\
If FFmpeg is installed in a non-standard location, set PKG_CONFIG_PATH:\n\
export PKG_CONFIG_PATH=\"/path/to/ffmpeg/lib/pkgconfig\""
);
}
const PKGCONFIG_LIBS: &[(&str, &str)] = &[
("libavformat", "61.0"),
("libavcodec", "61.0"),
("libavutil", "59.0"),
("libswscale", "8.0"),
("libswresample", "5.0"),
("libavfilter", "10.0"),
];
fn try_pkgconfig_unix() -> Option<Vec<String>> {
let mut include_paths = Vec::new();
let mut all_found = true;
for (lib, min_version) in PKGCONFIG_LIBS {
match pkg_config::Config::new()
.atleast_version(min_version)
.probe(lib)
{
Ok(library) => {
for path in &library.include_paths {
let path_str = path.to_string_lossy().to_string();
if !include_paths.contains(&path_str) {
include_paths.push(path_str);
}
}
}
Err(e) => {
println!("cargo:warning=pkg-config: {lib} not found: {e}");
all_found = false;
break;
}
}
}
if all_found { Some(include_paths) } else { None }
}
fn emit_api_cfg_flags(include_paths: &[String]) {
let swscale_major = read_version_major(include_paths, "libswscale");
if let Some(major) = swscale_major {
if major >= 9 {
println!("cargo:rustc-cfg=ffmpeg8");
}
} else {
println!(
"cargo:warning=Could not detect libswscale version; \
assuming FFmpeg 7.x (#define SWS_* constants)"
);
}
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if matches!(target_os.as_str(), "linux" | "macos") {
println!("cargo:rustc-cfg=ffmpeg_buffersrc_flag_u32");
}
}
fn read_version_major(include_paths: &[String], lib: &str) -> Option<u32> {
for base in include_paths {
let base = Path::new(base).join(lib);
let candidates = [base.join("version_major.h"), base.join("version.h")];
for path in &candidates {
let Ok(content) = std::fs::read_to_string(path) else {
continue;
};
let needle = format!(
"LIB{}_VERSION_MAJOR",
lib.trim_start_matches("lib").to_ascii_uppercase()
);
for line in content.lines() {
if line.contains(&needle) {
if let Some(val) = line.split_whitespace().last() {
if let Ok(n) = val.parse::<u32>() {
return Some(n);
}
}
}
}
}
}
None
}
fn generate_bindings(include_paths: &[String]) {
let mut builder = bindgen::Builder::default().header("wrapper.h");
for path in include_paths {
builder = builder.clang_arg(format!("-I{path}"));
}
let bindings = builder
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.allowlist_function("av_.*")
.allowlist_function("avformat_.*")
.allowlist_function("avcodec_.*")
.allowlist_function("sws_.*")
.allowlist_function("swr_.*")
.allowlist_type("AV.*")
.allowlist_type("Sws.*")
.allowlist_type("Swr.*")
.allowlist_var("AV_.*")
.allowlist_var("AVERROR.*")
.allowlist_var("AVSEEK_.*")
.allowlist_var("AVIO_.*")
.allowlist_var("SWS_.*")
.allowlist_var("SWR_.*")
.allowlist_function("avfilter_.*")
.allowlist_function("av_buffersrc_.*")
.allowlist_function("av_buffersink_.*")
.allowlist_type("AVFilter.*")
.allowlist_var("AV_BUFFERSRC_.*")
.allowlist_var("AV_BUFFERSINK_.*")
.derive_debug(true)
.derive_default(true)
.generate_comments(false)
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
println!("cargo:rerun-if-changed=wrapper.h");
}