//! Build script for `openzl-sys-rs`.
//!
//! The OpenZL C sources (plus the exact zstd and lz4 releases OpenZL pins) are
//! **vendored** under `openzl/`. This build therefore needs:
//!
//! * no network access,
//! * no `curl`, no `tar`, no `git`, no `cmake`, no shell — nothing is spawned,
//! * only a C compiler, which any `-sys` crate needs anyway.
//!
//! Everything below is a faithful port of what `openzl`'s own CMake did for the
//! `openzl` target, `deps/zstd/build/cmake` for `libzstd_static`, and
//! `deps/lz4/build/cmake` for `lz4_static`. See `openzl/VENDOR.md` for
//! provenance and for the flags each of those used.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
/// Upstream OpenZL commit the vendored tree was taken from: tag `v0.3.0`
/// (library 0.3.0, frame formats 8..=27), committed 2026-09-29.
const OPENZL_VERSION: &str = "32246b48faee46807f84183dac4db479089f5445";
const ZSTD_VERSION: &str = "1.5.7";
const LZ4_VERSION: &str = "1.10.0";
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=OPENZL_SYS_LIB_DIR");
// Escape hatch: link pre-built libraries instead of compiling the vendored
// tree. Loud on purpose — a silent fallback is how a build ends up linking
// something nobody chose.
if let Ok(dir) = env::var("OPENZL_SYS_LIB_DIR") {
let dir = PathBuf::from(&dir);
for lib in ["libopenzl.a", "libzstd.a", "liblz4.a"] {
assert!(
dir.join(lib).exists(),
"OPENZL_SYS_LIB_DIR={} is set but {} is not there — refusing to \
half-link. Unset it to build the vendored sources instead.",
dir.display(),
lib
);
}
eprintln!(
"openzl-sys-rs: OPENZL_SYS_LIB_DIR={} — using pre-built libraries, \
vendored sources NOT compiled",
dir.display()
);
println!("cargo:rustc-link-search=native={}", dir.display());
println!("cargo:rustc-link-lib=static=openzl");
println!("cargo:rustc-link-lib=static=zstd");
println!("cargo:rustc-link-lib=static=lz4");
emit_system_libs();
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let vendor = manifest_dir.join("openzl");
// A missing vendor tree must be a hard, explanatory failure — not a
// mysterious "file not found" 200 lines into a compile.
assert!(
vendor.join("src/openzl").is_dir(),
"vendored OpenZL sources missing at {} — this crate ships them; a \
checkout that lacks them is broken, it is not a signal to download \
anything.",
vendor.display()
);
assert!(
vendor.join("deps/zstd/lib/zstd.h").exists(),
"vendored zstd {} missing at {}",
ZSTD_VERSION,
vendor.join("deps/zstd/lib").display()
);
assert!(
vendor.join("deps/lz4/lib/lz4.h").exists(),
"vendored lz4 {} missing at {}",
LZ4_VERSION,
vendor.join("deps/lz4/lib").display()
);
println!("cargo:rerun-if-changed={}", vendor.join("src").display());
println!("cargo:rerun-if-changed={}", vendor.join("include").display());
println!("cargo:rerun-if-changed={}", vendor.join("deps").display());
// CMake `Release` added -DNDEBUG; `Debug` did not. Mirror that so the
// library behaves as it did under the old cmake-driven build.
let ndebug = env::var("OPT_LEVEL").map(|o| o != "0").unwrap_or(true);
build_zstd(&vendor, ndebug);
build_lz4(&vendor, ndebug);
build_openzl(&vendor, &out_dir, ndebug);
emit_system_libs();
println!("cargo:vendored=1");
println!("cargo:openzl_version={OPENZL_VERSION}");
println!("cargo:zstd_version={ZSTD_VERSION}");
println!("cargo:lz4_version={LZ4_VERSION}");
}
/// libm and pthreads. OpenZL's CMake linked `m` and `Threads::Threads`;
/// zstd is built with `-DZSTD_MULTITHREAD`, which needs pthreads.
///
/// Note: the 0.2.0 build script also emitted `-lstdc++`. It was never needed —
/// the `openzl` target is compiled with `OPENZL_BUILD_CPP=OFF` and is pure C;
/// `nm --undefined-only libopenzl.a` reports zero C++ symbols. Dropping it
/// removes the libstdc++ requirement from the build.
fn emit_system_libs() {
if env::var("CARGO_CFG_TARGET_FAMILY").as_deref() != Ok("unix") {
return;
}
// MUSL HAS NO SEPARATE libm OR libpthread — both are inside libc, and musl
// ships no `libm.a`/`libpthread.a` for a linker to find. Emitting them is
// not merely redundant there, it is FATAL: the link ends with
// rust-lld: error: unable to find library -lm
// rust-lld: error: unable to find library -lpthread
// and the whole static appliance binary fails to link. Measured on oden
// 2026-08-14 building holger's `mvp-server-musl` atom — the wall that had
// been read as "the musl cross toolchain is missing" was partly this.
//
// glibc still needs them named: it really does ship them as separate
// archives, and dropping them there would break the ordinary build.
if env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("musl") {
return;
}
println!("cargo:rustc-link-lib=m");
println!("cargo:rustc-link-lib=pthread");
}
/// zstd, exactly as `deps/zstd/build/cmake` built `libzstd_static`:
/// `-DXXH_NAMESPACE=ZSTD_ -DZSTD_LEGACY_SUPPORT=0 -DZSTD_MULTITHREAD`, over
/// `lib/{common,compress,decompress,dictBuilder}`. Legacy support is off, so
/// `lib/legacy` is not vendored.
fn build_zstd(vendor: &Path, ndebug: bool) {
let lib = vendor.join("deps/zstd/lib");
let mut build = cc::Build::new();
build
.include(&lib)
.include(lib.join("common"))
.define("XXH_NAMESPACE", "ZSTD_")
.define("ZSTD_LEGACY_SUPPORT", "0")
.define("ZSTD_MULTITHREAD", None)
.flag_if_supported("-std=gnu11")
.flag_if_supported("-Wa,--noexecstack")
.warnings(false);
if ndebug {
build.define("NDEBUG", None);
}
let mut sources = Vec::new();
for sub in ["common", "compress", "decompress", "dictBuilder"] {
collect(&lib.join(sub), "c", &mut sources);
}
// huf_decompress_amd64.S self-guards on __x86_64__, so it is safe to feed
// to any non-MSVC toolchain; MSVC cannot assemble it at all.
let asm = lib.join("decompress/huf_decompress_amd64.S");
if asm.exists() && env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("msvc") {
sources.push(asm);
} else {
build.define("ZSTD_DISABLE_ASM", None);
}
sources.sort();
assert!(
sources.len() > 20,
"vendored zstd looks truncated: only {} translation units found under {}",
sources.len(),
lib.display()
);
build.files(&sources);
build.compile("zstd");
}
/// lz4, exactly as `deps/lz4/build/cmake` built `lz4_static`: every `lib/*.c`
/// with `-DXXH_NAMESPACE=LZ4_`.
fn build_lz4(vendor: &Path, ndebug: bool) {
let lib = vendor.join("deps/lz4/lib");
let mut build = cc::Build::new();
build
.include(&lib)
.define("XXH_NAMESPACE", "LZ4_")
.warnings(false);
if ndebug {
build.define("NDEBUG", None);
}
let mut sources = Vec::new();
collect(&lib, "c", &mut sources);
sources.sort();
assert!(
sources.len() >= 5,
"vendored lz4 looks truncated: only {} translation units found under {}",
sources.len(),
lib.display()
);
build.files(&sources);
build.compile("lz4");
}
/// The `openzl` target: `file(GLOB_RECURSE sources "src/*.c")`, compiled as
/// C11 with `-D_GNU_SOURCE -D_REENTRANT` and the six include directories the
/// CMake build put on the command line.
fn build_openzl(vendor: &Path, out_dir: &Path, ndebug: bool) {
let config_dir = out_dir.join("include");
write_zl_config(&config_dir);
let src = vendor.join("src");
let mut build = cc::Build::new();
build
// Generated header dir goes first, as CMake put it.
.include(&config_dir)
.include(vendor)
.include(vendor.join("include"))
.include(&src)
.include(vendor.join("deps/lz4/lib"))
.include(vendor.join("deps/zstd/lib"))
.define("_GNU_SOURCE", None)
.define("_REENTRANT", None)
.flag_if_supported("-std=gnu11")
.warnings(false);
if ndebug {
build.define("NDEBUG", None);
}
let mut sources = Vec::new();
collect(&src, "c", &mut sources);
sources.sort();
// The upstream tree has 279 .c files under src/ (v0.3.0; 0.2.1 had 247).
// A glob that silently returns a handful would still "build" and then fail
// at link with a wall of undefined symbols; catch it here where the message
// is useful.
assert!(
sources.len() > 200,
"vendored OpenZL looks truncated: only {} .c files found under {} \
(expected ~279)",
sources.len(),
src.display()
);
build.files(&sources);
build.compile("openzl");
}
/// Reproduces `configure_file(build-scripts/cmake/zl_config.h.cmake ...)`.
///
/// Both knobs are 0 in the configuration this crate builds:
/// * `ZL_ALLOW_INTROSPECTION` — CMake was invoked with
/// `-DOPENZL_ALLOW_INTROSPECTION=OFF`.
/// * `ZL_HAVE_X86_64_ASM` — upstream's `OpenZLConfigChecks.cmake` sets
/// `OPENZL_HAVE_X86_64_ASM`, but `zl_config.h.cmake` and `CMakeLists.txt`
/// both test `ZL_HAVE_X86_64_ASM`, which is never assigned. The x86-64
/// assembly path is therefore off in every CMake build of this crate, and
/// `src/openzl/fse/decompress/huf_decompress_amd64.S` is never compiled.
/// Keeping it off here is what makes this build equivalent to the 0.2.0
/// artifact rather than a guess at what upstream meant.
fn write_zl_config(config_dir: &Path) {
let dir = config_dir.join("openzl");
fs::create_dir_all(&dir).expect("failed to create generated include dir");
let path = dir.join("zl_config.h");
let contents = "\
// Copyright (c) Meta Platforms, Inc. and affiliates.
// Generated by openzl-sys-rs/build.rs from OpenZL's zl_config.h.cmake.
#ifndef OPENZL_ZS2_CONFIG_H
#define OPENZL_ZS2_CONFIG_H
// clang-format off
// BEGIN CODEMOD DEFINES
#ifndef OPENZL_HAVE_X86_64_ASM
#define OPENZL_HAVE_X86_64_ASM ZL_HAVE_X86_64_ASM // TODO(T223464378): Delete
#endif
// END CODEMOD DEFINES
// clang-format on
#define ZL_ALLOW_INTROSPECTION 0
#define ZL_HAVE_X86_64_ASM 0
#endif
";
// Only rewrite when it differs, so touching build.rs does not invalidate
// 279 object files.
if fs::read_to_string(&path).ok().as_deref() != Some(contents) {
fs::write(&path, contents).expect("failed to write zl_config.h");
}
}
/// Recursive equivalent of CMake's `file(GLOB_RECURSE ... "*.<ext>")`.
fn collect(dir: &Path, ext: &str, out: &mut Vec<PathBuf>) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(e) => panic!("cannot read vendored source dir {}: {e}", dir.display()),
};
for entry in entries {
let path = entry.expect("failed to read dir entry").path();
if path.is_dir() {
collect(&path, ext, out);
} else if path.extension().and_then(|e| e.to_str()) == Some(ext) {
out.push(path);
}
}
}