Gunnar

mikronova/openzl-sys-rs

build.rs

//! 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);
        }
    }
}