use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use crate::cargo_feature_enabled;
use crate::Library;
static EXTERNAL_BUILD_LIBS: &[(&str, &str)] = &[
("GNUTLS", "gnutls"),
("OPENSSL", "openssl"),
("FONTCONFIG", "fontconfig"),
("FREI0R", "frei0r"),
("LADSPA", "ladspa"),
("ASS", "libass"),
("FREETYPE", "libfreetype"),
("FRIBIDI", "libfribidi"),
("OPENCV", "libopencv"),
("VMAF", "libvmaf"),
("AACPLUS", "libaacplus"),
("CELT", "libcelt"),
("CODEC2", "libcodec2"),
("DAV1D", "libdav1d"),
("DAVS2", "libdavs2"),
("DCADEC", "libdcadec"),
("FAAC", "libfaac"),
("FDK_AAC", "libfdk-aac"),
("GSM", "libgsm"),
("ILBC", "libilbc"),
("JXL", "libjxl"),
("KVAZAAR", "libkvazaar"),
("LC3", "liblc3"),
("LCEVC_DEC", "liblcevc-dec"),
("MP3LAME", "libmp3lame"),
("MPEGHDEC", "libmpeghdec"),
("OAPV", "liboapv"),
("OPENCORE_AMRNB", "libopencore-amrnb"),
("OPENCORE_AMRWB", "libopencore-amrwb"),
("OPENH264", "libopenh264"),
("OPENH265", "libopenh265"),
("OPENJPEG", "libopenjpeg"),
("OPENMPT", "libopenmpt"),
("OPUS", "libopus"),
("RAV1E", "librav1e"),
("SCHROEDINGER", "libschroedinger"),
("SHINE", "libshine"),
("SNAPPY", "libsnappy"),
("SPEEX", "libspeex"),
("STAGEFRIGHT_H264", "libstagefright-h264"),
("SVTAV1", "libsvtav1"),
("SVTJPEGXS", "libsvtjpegxs"),
("THEORA", "libtheora"),
("TWOLAME", "libtwolame"),
("UAVS3D", "libuavs3d"),
("UTVIDEO", "libutvideo"),
("VO_AACENC", "libvo-aacenc"),
("VO_AMRWBENC", "libvo-amrwbenc"),
("VORBIS", "libvorbis"),
("VPX", "libvpx"),
("VVENC", "libvvenc"),
("WAVPACK", "libwavpack"),
("WEBP", "libwebp"),
("X264", "libx264"),
("X265", "libx265"),
("XEVE", "libxeve"),
("XEVD", "libxevd"),
("XAVS", "libxavs"),
("XAVS2", "libxavs2"),
("AVS", "libavs"),
("XVID", "libxvid"),
("SMBCLIENT", "libsmbclient"),
("SSH", "libssh"),
];
fn get_repo_url() -> String {
env::var("FFMPEG_GIT_URL").unwrap_or("https://github.com/FFmpeg/FFmpeg".into())
}
fn get_newest_patch_version() -> String {
let crate_ffmpeg_version = env!("CARGO_PKG_VERSION")
.split_once("+ffmpeg-")
.expect("crate version follows v1.2.3+ffmpeg-4.5 format")
.1;
let Output { status, stdout, .. } = Command::new("git")
.arg("ls-remote")
.arg("-q")
.arg("--tags")
.arg("--refs")
.arg(get_repo_url())
.arg(format!("n{}*", crate_ffmpeg_version))
.output()
.expect("can run git ls-remote");
assert!(
status.success(),
"git ls-remote returned non-zero exit code"
);
String::from_utf8(stdout)
.expect("git ls-remote output is utf8")
.lines()
.filter_map(|line| line.split_once("refs/tags/n"))
.map(|(_hash, version)| version)
.filter(|ver| !ver.contains("-dev"))
.max() .expect("matching non-dev tag exists")
.to_string()
}
fn fetch(source_dir: &Path, ffmpeg_version: &str) -> io::Result<()> {
let _ = std::fs::remove_dir_all(source_dir);
let status = Command::new("git")
.arg("clone")
.arg("--depth=1")
.arg("-b")
.arg(format!("n{ffmpeg_version}"))
.arg(get_repo_url())
.arg(source_dir)
.status()?;
if status.success() {
Ok(())
} else {
Err(io::Error::other("fetch failed"))
}
}
pub fn build(libraries: &[Library], out_dir: &Path) -> io::Result<PathBuf> {
println!("cargo::rerun-if-env-changed=FFMPEG_GIT_URL");
let ffmpeg_version = get_newest_patch_version();
let source_dir = out_dir.join(format!("ffmpeg-{ffmpeg_version}"));
let install_dir = out_dir.join("dist");
if install_dir.join("lib").join("libavutil.a").exists() {
rustc_link_extralibs(&source_dir);
return Ok(install_dir);
}
fetch(&source_dir, &ffmpeg_version)?;
let library_flags = libraries.iter().map(|lib| format!("--enable-{}", lib.name));
let configure_path = source_dir.join("configure");
assert!(configure_path.exists());
let mut configure = Command::new(configure_path);
configure.current_dir(&source_dir);
configure.arg(format!("--prefix={}", install_dir.to_string_lossy()));
configure.args(library_flags);
if env::var("TARGET").unwrap() != env::var("HOST").unwrap() {
let cc = cc::Build::new();
let compiler = cc.get_compiler();
let compiler = compiler.path().file_stem().unwrap().to_str().unwrap();
let suffix_pos = compiler.rfind('-').unwrap(); let prefix = compiler[0..suffix_pos].trim_end_matches("-wr");
configure.arg(format!("--cross-prefix={}-", prefix));
configure.arg(format!(
"--arch={}",
env::var("CARGO_CFG_TARGET_ARCH").unwrap()
));
configure.arg(format!(
"--target_os={}",
env::var("CARGO_CFG_TARGET_OS").unwrap()
));
}
if env::var("DEBUG").is_ok() {
configure.arg("--enable-debug");
configure.arg("--disable-stripping");
} else {
configure.arg("--disable-debug");
configure.arg("--enable-stripping");
}
configure.arg("--enable-static");
configure.arg("--disable-shared");
configure.arg("--enable-pic");
configure.arg("--disable-autodetect");
configure.arg("--disable-programs");
configure.arg("--disable-doc");
configure.switch("BUILD_LICENSE_GPL", "gpl");
configure.switch("BUILD_LICENSE_VERSION3", "version3");
configure.switch("BUILD_LICENSE_NONFREE", "nonfree");
for (cargo_feat, option_name) in EXTERNAL_BUILD_LIBS {
configure.enable(&format!("BUILD_LIB_{cargo_feat}"), option_name);
}
configure.enable("BUILD_DRM", "libdrm");
configure.enable("BUILD_NVENC", "nvenc");
let output = configure.output()?;
if !output.status.success() {
println!("configure: {}", String::from_utf8_lossy(&output.stdout));
return Err(io::Error::other(format!(
"configure failed {}",
String::from_utf8_lossy(&output.stderr)
)));
}
if !Command::new("make")
.arg("install")
.env("MAKEFLAGS", env::var("CARGO_MAKEFLAGS").unwrap())
.current_dir(&source_dir)
.status()?
.success()
{
return Err(io::Error::other("make install failed"));
}
rustc_link_extralibs(&source_dir);
Ok(install_dir)
}
fn rustc_link_extralibs(source_dir: &Path) {
let config_mak = source_dir.join("ffbuild").join("config.mak");
let file = File::open(config_mak).unwrap();
let reader = BufReader::new(file);
let extra_libs = reader
.lines()
.map(|line| line.expect("config.mak contains valid utf8"))
.find(|line| line.starts_with("EXTRALIBS="))
.expect("config.mak contains EXTRALIBS= line");
let include_libs = extra_libs
.strip_prefix("EXTRALIBS=")
.unwrap()
.split_ascii_whitespace()
.filter_map(|v| v.strip_prefix("-l"));
for lib in include_libs {
println!("cargo::rustc-link-lib={lib}");
}
}
trait FFmpegConfigure {
fn switch(&mut self, feature: &str, option_name: &str);
fn enable(&mut self, feature: &str, option_name: &str);
}
impl FFmpegConfigure for Command {
fn switch(&mut self, feature: &str, option_name: &str) {
let arg = if cargo_feature_enabled(feature) {
format!("--enable-{option_name}")
} else {
format!("--disable-{option_name}")
};
self.arg(arg);
}
fn enable(&mut self, feature: &str, option_name: &str) {
if cargo_feature_enabled(feature) {
self.arg(format!("--enable-{option_name}"));
}
}
}