use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const FFMPEG_LIBS: &[&str] = &[
"avdevice",
"avfilter",
"avformat",
"avcodec",
"swscale",
"swresample",
"avutil",
];
const BUNDLED_LIBS: &[&str] = &[
"mp3lame",
"opus",
"speex",
"vorbisenc",
"vorbis",
"ogg",
"ssl",
"crypto",
];
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let project_root = cmake_source_dir(&manifest_dir);
rerun_if_changed(&manifest_dir, &project_root);
let install_dir = build_native(&project_root);
let lib_dir = install_dir.join("lib");
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=static=avioflow");
for lib in FFMPEG_LIBS.iter().chain(BUNDLED_LIBS) {
if lib_dir.join(format!("lib{lib}.a")).exists() {
println!("cargo:rustc-link-lib=static={lib}");
}
}
for lib in system_libs(&lib_dir) {
println!("cargo:rustc-link-lib={lib}");
}
if let Some(stdlib) = cxx_stdlib() {
println!("cargo:rustc-link-lib={stdlib}");
}
println!("cargo:root={}", install_dir.display());
println!("cargo:include={}", install_dir.join("include").display());
}
fn cmake_source_dir(manifest_dir: &Path) -> PathBuf {
let vendored = manifest_dir.join("native");
if vendored.join("CMakeLists.txt").exists() {
println!("cargo:rerun-if-changed={}", vendored.display());
return vendored;
}
let repo_root = manifest_dir
.parent()
.expect("crate directory must have a parent")
.to_path_buf();
if repo_root.join("CMakeLists.txt").exists() {
return repo_root;
}
panic!(
"Could not find the avioflow CMake project. Looked for {} and {}. \
When building from a repository checkout, keep the crate in rust/; \
when packaging, run scripts/vendor_rust_sources.py first.",
vendored.join("CMakeLists.txt").display(),
repo_root.join("CMakeLists.txt").display()
);
}
fn rerun_if_changed(manifest_dir: &Path, project_root: &Path) {
println!(
"cargo:rerun-if-changed={}",
manifest_dir.join("build.rs").display()
);
println!(
"cargo:rerun-if-changed={}",
project_root.join("CMakeLists.txt").display()
);
println!(
"cargo:rerun-if-changed={}",
project_root.join("avioflow").display()
);
println!("cargo:rerun-if-env-changed=AVIOFLOW_CMAKE_ARGS");
}
fn build_native(project_root: &Path) -> PathBuf {
let mut config = cmake::Config::new(project_root);
config
.define("BUILD_SHARED_LIBS", "OFF")
.define("BUILD_TESTS", "OFF")
.define("BUILD_TESTING", "OFF")
.define("ENABLE_BINARY", "OFF")
.define("ENABLE_PYTHON", "OFF")
.define("ENABLE_NODE_JS", "OFF")
.define("ENABLE_JAVA", "OFF")
.profile("Release");
if let Ok(extra) = env::var("AVIOFLOW_CMAKE_ARGS") {
for arg in extra.split_whitespace() {
if let Some((key, value)) = arg.trim_start_matches("-D").split_once('=') {
config.define(key, value);
}
}
}
config.build()
}
fn system_libs(lib_dir: &Path) -> Vec<String> {
let mut libs = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for known in FFMPEG_LIBS.iter().chain(BUNDLED_LIBS) {
seen.insert((*known).to_string());
}
let pkgconfig_dir = lib_dir.join("pkgconfig");
if let Ok(entries) = fs::read_dir(&pkgconfig_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("pc") {
continue;
}
let Ok(contents) = fs::read_to_string(&path) else {
continue;
};
for line in contents.lines() {
let rest = match line.split_once(':') {
Some(("Libs" | "Libs.private", rest)) => rest,
_ => continue,
};
for token in rest.split_whitespace() {
let Some(name) = token.strip_prefix("-l") else {
continue;
};
if lib_dir.join(format!("lib{name}.a")).exists() {
continue;
}
if seen.insert(name.to_string()) {
libs.push(name.to_string());
}
}
}
}
}
for fallback in ["m", "pthread", "dl"] {
if seen.insert(fallback.to_string()) {
libs.push(fallback.to_string());
}
}
libs
}
fn cxx_stdlib() -> Option<&'static str> {
let target = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
match target.as_str() {
"macos" | "ios" => Some("c++"),
"windows" => None,
_ => Some("stdc++"),
}
}