Files
oak-editor/crates/oakffmpeg-link/build.rs
T
Mike-Solar 61da70ecf8 fix(build): launch crash — @rpath/libz.1.dylib had no LC_RPATH
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.
2026-08-17 19:42:45 +08:00

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
}