The static FFmpeg's external codec libs pull in -lz, which on this toolchain resolves to a copy whose install name is @rpath/libz.1.dylib (zlib-ng-compat); without an LC_RPATH entry all three binaries died in dyld at startup. The app/cli/worker build scripts now emit -Wl,-rpath,/usr/lib. Also: FFMPEG_DIR moves into the committed .cargo/config.toml as a workspace-relative [env] entry — ffmpeg-sys-next's build script cannot read .env files, and without it the crate silently linked the shared Homebrew FFmpeg while oakffmpeg-link emitted the static transitive flags (mixed linkage). docs/build.md updated.
181 lines
6.8 KiB
Rust
181 lines
6.8 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
|
// Copyright (C) 2026 Oak Team
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Build-time link configuration for the `oakffmpeg-link` crate.
|
|
//!
|
|
//! ffmpeg-sys-next only emits the link directives for FFmpeg's own
|
|
//! libraries. When FFmpeg is linked STATICALLY (the project FFmpeg built
|
|
//! by tooling/ffmpeg/build-ffmpeg.sh, pointed at via `FFMPEG_DIR`), its
|
|
//! archives reference every external codec library they were configured
|
|
//! with (x264, x265, dav1d, ..., plus system frameworks on macOS), and
|
|
//! the final link fails with undefined symbols unless those transitive
|
|
//! dependencies are linked too. FFmpeg's own `.pc` files carry the exact
|
|
//! list in `Libs.private`, so this script asks pkg-config for the static
|
|
//! link line and forwards it to cargo.
|
|
//!
|
|
//! The project FFmpeg (built by tooling/ffmpeg/build-ffmpeg.sh, pointed
|
|
//! at via `FFMPEG_DIR`) is MANDATORY: failing loudly beats silently
|
|
//! binding a system pkg-config FFmpeg whose .pc may reference stale
|
|
//! paths (observed: a Homebrew upgrade of dav1d left ffmpeg's own .pc
|
|
//! pointing at a deleted Cellar dir, breaking the link).
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn main() {
|
|
let dir = match env_or_dotenv("FFMPEG_DIR") {
|
|
Some(dir) => dir,
|
|
None => panic!(
|
|
"FFMPEG_DIR is not set. Oak links FFmpeg statically; build the project FFmpeg \
|
|
first:\n tooling/install-deps.sh\n tooling/ffmpeg/build-ffmpeg.sh\n export \
|
|
FFMPEG_DIR=$(pwd)/.cache/ffmpeg\n(see docs/build.md). IDEs that cannot inject \
|
|
environment variables into cargo (e.g. RustRover) can use a .env file at the \
|
|
workspace root with FFMPEG_DIR=<absolute path>."
|
|
),
|
|
};
|
|
let pc_dir = PathBuf::from(&dir).join("lib").join("pkgconfig");
|
|
assert!(
|
|
pc_dir.exists(),
|
|
"FFMPEG_DIR={dir} has no lib/pkgconfig — point it at a full install prefix \
|
|
(the output of tooling/ffmpeg/build-ffmpeg.sh)"
|
|
);
|
|
println!("cargo:rerun-if-env-changed=FFMPEG_DIR");
|
|
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
println!(
|
|
"cargo:rerun-if-changed={}",
|
|
manifest.join("../..").join(".env").display()
|
|
);
|
|
|
|
let pkg_path = env_or_dotenv("PKG_CONFIG_PATH").unwrap_or_default();
|
|
let output = Command::new("pkg-config")
|
|
.arg("--static")
|
|
.arg("--libs")
|
|
.args([
|
|
"libavformat",
|
|
"libavcodec",
|
|
"libavfilter",
|
|
"libavdevice",
|
|
"libavutil",
|
|
"libswscale",
|
|
"libswresample",
|
|
])
|
|
.env(
|
|
"PKG_CONFIG_PATH",
|
|
format!(
|
|
"{}{}{}",
|
|
pc_dir.display(),
|
|
if pkg_path.is_empty() { "" } else { ":" },
|
|
pkg_path
|
|
),
|
|
)
|
|
.output()
|
|
.expect("pkg-config is required when FFMPEG_DIR is set");
|
|
if !output.status.success() {
|
|
panic!(
|
|
"pkg-config --static --libs failed for the FFMPEG_DIR install: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
// System libraries (z, m, bz2, iconv) must come from the OS, not from
|
|
// a package manager's keg: Homebrew's zlib carries an @rpath install
|
|
// name, and linking it without an rpath entry breaks the binary at
|
|
// launch (dyld: Library not loaded: @rpath/libz.1.dylib). Put /usr/lib
|
|
// first in the search order so -lz resolves to the system copy.
|
|
#[cfg(target_os = "macos")]
|
|
println!("cargo:rustc-link-search=native=/usr/lib");
|
|
|
|
for token in String::from_utf8_lossy(&output.stdout).split_whitespace() {
|
|
if let Some(path) = token.strip_prefix("-L") {
|
|
println!("cargo:rustc-link-search=native={path}");
|
|
} else if let Some(lib) = token.strip_prefix("-l") {
|
|
// System libs (m, z, bz2, iconv, ...) and externals alike; the
|
|
// linker picks .a or .dylib per -L search order.
|
|
println!("cargo:rustc-link-lib={lib}");
|
|
} else if token == "-framework" {
|
|
// Handled on the next token (see below).
|
|
} else if let Some(framework) = token.strip_prefix("-framework=") {
|
|
println!("cargo:rustc-link-lib=framework={framework}");
|
|
}
|
|
// Bare tokens after "-framework" are handled by the stateful pass
|
|
// below; pkg-config prints "-framework Foo" as two tokens.
|
|
}
|
|
// Second pass for the two-token "-framework Foo" form.
|
|
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
|
|
let tokens: Vec<&str> = stdout.split_whitespace().collect();
|
|
for pair in tokens.windows(2) {
|
|
if pair[0] == "-framework" {
|
|
println!("cargo:rustc-link-lib=framework={}", pair[1]);
|
|
}
|
|
}
|
|
|
|
// Some codec libraries are C++ (svt-av1's JsonHelper, ...); their
|
|
// archives reference the C++ standard library, which pkg-config's
|
|
// Libs.private does not list. Link it explicitly.
|
|
if cfg!(target_os = "macos") {
|
|
println!("cargo:rustc-link-lib=c++");
|
|
} else if cfg!(all(target_os = "linux", target_env = "gnu")) {
|
|
println!("cargo:rustc-link-lib=stdc++");
|
|
}
|
|
|
|
// Some externals (Homebrew lame, snappy) reach the FFmpeg archives via
|
|
// the configure-time --extra-ldflags instead of Requires.private, so
|
|
// their -L never shows up above; add the Homebrew lib dir as a
|
|
// fallback search path.
|
|
#[cfg(target_os = "macos")]
|
|
for prefix in ["/opt/homebrew/lib", "/usr/local/lib"] {
|
|
if std::path::Path::new(prefix).exists() {
|
|
println!("cargo:rustc-link-search=native={prefix}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reads `key` from the process environment, falling back to the `.env`
|
|
/// file at the workspace root (this crate lives in crates/oakffmpeg-link,
|
|
/// so the root is two levels up). The .env syntax is the common one:
|
|
/// `KEY=value` lines, optional `export ` prefix, `#` comments, optional
|
|
/// matching single/double quotes around the value.
|
|
///
|
|
/// IDEs that cannot inject environment variables into the cargo
|
|
/// invocation (RustRover) use the .env fallback for `FFMPEG_DIR`.
|
|
fn env_or_dotenv(key: &str) -> Option<String> {
|
|
if let Ok(v) = std::env::var(key) {
|
|
return Some(v);
|
|
}
|
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
let content = std::fs::read_to_string(root.join(".env")).ok()?;
|
|
for line in content.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let line = line.strip_prefix("export ").unwrap_or(line);
|
|
let Some((k, v)) = line.split_once('=') else {
|
|
continue;
|
|
};
|
|
if k.trim() == key {
|
|
let v = v.trim();
|
|
let v = v
|
|
.strip_prefix('"')
|
|
.and_then(|s| s.strip_suffix('"'))
|
|
.or_else(|| v.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
|
|
.unwrap_or(v);
|
|
return Some(v.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|