diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80cb4187..edd05ca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: true + - name: test forced system libz + run: ci/test-force-system.bash # Ensures --all-features builds correctly, the current logic will mean it # uses stock zlib, not cmake nor cc - run: | diff --git a/build.rs b/build.rs index 1a1f37a7..2f14d0fa 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,44 @@ +//! # Build behavior +//! +//! This script selects and links one zlib implementation. Its decisions are made in the +//! following order: +//! +//! 1. `LIBZ_SYS_STATIC=0` selects the system libz. This hard override skips implementation +//! features, compiler probing, and bundled builds, while allowing pkg-config or vcpkg to emit +//! available link and include metadata. The caller must ultimately provide a linkable libz. +//! 2. The `zlib-ng` or `zlib-ng-no-cmake-experimental-community-maintained` feature builds +//! zlib-ng in compatibility mode, unless `stock-zlib` is also enabled or the target is +//! `wasm32-unknown-unknown`. The former uses CMake and the latter uses `cc`. +//! 3. Android, Haiku, and OpenHarmony targets link `z` directly. +//! 4. `LIBZ_SYS_STATIC=1` requests bundled stock zlib. Any other value falls back to the +//! `static` feature setting. +//! 5. Unless static linking was requested, the target is MSVC, or both host and target are +//! FreeBSD or DragonFly, `pkg-config` probes `zlib`. It emits Cargo link metadata and this +//! script forwards non-empty include paths, but system library directories are omitted. +//! Probe failure is only a warning. +//! 6. Windows targets try vcpkg. A successful lookup emits its link metadata and include paths +//! and completes the script. +//! 7. MSVC, MinGW, and explicit static builds compile the bundled stock zlib unless the system +//! override is active. The override links `z` directly; remaining targets first compile and +//! link `src/smoke.c` with `-lz`, then link `z` on success or bundle it on failure. +//! +//! Except for the `0` hard override, `LIBZ_SYS_STATIC` and the `static` feature are preferences +//! rather than guarantees because the earlier implementation and platform branches take +//! precedence. Changing `LIBZ_SYS_STATIC` reruns the script. +//! +//! ## Bundled stock zlib +//! +//! The bundled build disables warnings and compiles into `$OUT_DIR/lib`. It defines `STDC` on +//! every target. Non-Windows targets also define `_LARGEFILE64_SOURCE` and use hidden symbol +//! visibility. `wasm32-unknown-unknown` additionally defines `Z_SOLO` and omits the `gz*` +//! sources; other targets include them. An AArch64 target with the `crc` target feature enables +//! the compiler's Armv8 CRC support when available. +//! +//! After compilation, `zlib.h` and `zconf.h` are copied to `$OUT_DIR/include`, a `zlib.pc` file +//! is generated in `$OUT_DIR/lib/pkgconfig`, and Cargo receives the root, native library search +//! path, and include directory. Cargo also reruns this script when `build.rs`, `zng/cmake.rs`, or +//! `zng/cc.rs` changes. + use std::env; use std::fs; use std::path::PathBuf; @@ -11,6 +52,15 @@ fn main() { let host = env::var("HOST").unwrap(); let target = env::var("TARGET").unwrap(); + let link_static = option_env!("LIBZ_SYS_STATIC") + .and_then(|s| s.parse::().ok()) + .and_then(|b| match b { + 0 => Some(false), + 1 => Some(true), + _ => None, + }); + let force_system = link_static == Some(false); + let host_and_target_contain = |s| host.contains(s) && target.contains(s); let want_ng = cfg!(any( @@ -18,7 +68,7 @@ fn main() { feature = "zlib-ng-no-cmake-experimental-community-maintained" )) && !cfg!(feature = "stock-zlib"); - if want_ng && target != "wasm32-unknown-unknown" { + if want_ng && !force_system && target != "wasm32-unknown-unknown" { return build_zlib_ng(&target, true); } @@ -30,7 +80,7 @@ fn main() { return; } - let want_static = should_link_static(); + let want_static = link_static.unwrap_or(cfg!(feature = "static")); // Don't run pkg-config if we're linking statically (we'll build below) and // also don't run pkg-config on FreeBSD/DragonFly. That'll end up printing // `-L /usr/lib` which wreaks havoc with linking to an OpenSSL in /usr/local/lib @@ -79,7 +129,11 @@ fn main() { // - MSVC basically never has zlib preinstalled // - MinGW picks up a bunch of weird paths we don't like // - Explicit opt-in via `want_static` - if target.contains("msvc") || target.contains("pc-windows-gnu") || want_static { + // + // An explicit system override takes precedence over all three. + if !force_system + && (target.contains("msvc") || target.contains("pc-windows-gnu") || want_static) + { return build_zlib(&mut cfg, &target); } @@ -87,9 +141,9 @@ fn main() { // Almost all platforms here ship libz by default, but some don't have // pkg-config files that we would find above. // - // In any case test if zlib is actually installed and if so we link to it, - // otherwise continue below to build things. - if zlib_installed(&mut cfg) { + // In any case link an explicitly requested system zlib, or test if zlib is + // installed and link to it, otherwise continue below to build things. + if force_system || zlib_installed(&mut cfg) { println!("cargo:rustc-link-lib=z"); return; } @@ -252,22 +306,3 @@ fn zlib_installed(cfg: &mut cc::Build) -> bool { false } - -/// The environment variable `LIBZ_SYS_STATIC` is first checked for a value of `0` (false) or `1` (true), -/// before considering the `static` feature when no explicit ENV value was detected. -/// When `libz-sys` is a transitive dependency from a crate that forces static linking via the `static` feature, -/// this enables the build environment to revert that preference via `LIBZ_SYS_STATIC=0`. -/// The default is otherwise `false`. -fn should_link_static() -> bool { - let has_static_env: Option<&'static str> = option_env!("LIBZ_SYS_STATIC"); - let has_static_cfg = cfg!(feature = "static"); - - has_static_env - .and_then(|s: &str| s.parse::().ok()) - .and_then(|b| match b { - 0 => Some(false), - 1 => Some(true), - _ => None, - }) - .unwrap_or(has_static_cfg) -} diff --git a/ci/test-force-system.bash b/ci/test-force-system.bash new file mode 100755 index 00000000..6aa648ce --- /dev/null +++ b/ci/test-force-system.bash @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Verify that LIBZ_SYS_STATIC=0 overrides the static feature and selects system libz +# without compiling or emitting metadata for the bundled library. +set -euo pipefail + +target_dir=target/force-system-libz + +CC=false LIBZ_SYS_STATIC=0 CARGO_TARGET_DIR="$target_dir" \ + cargo test --features static --no-run + +build_output=$(find "$target_dir/debug/build" -path '*/libz-sys-*/output' -print -quit) +if [[ -z "$build_output" ]]; then + echo 'error: libz-sys build-script output was not found' >&2 + exit 1 +fi + +if ! grep -Fxq 'cargo:rustc-link-lib=z' "$build_output"; then + echo "error: $build_output does not request system libz" >&2 + exit 1 +fi + +if grep -Fxq 'cargo:rustc-link-lib=static=z' "$build_output"; then + echo "error: $build_output requests static libz" >&2 + exit 1 +fi + +archive=$(find "$target_dir" -name libz.a -print -quit) +if [[ -n "$archive" ]]; then + echo "error: bundled libz archive was produced at $archive" >&2 + exit 1 +fi + +echo 'LIBZ_SYS_STATIC=0 linked system libz without building bundled libz.a'