use std::{ env, fs, io::Write as _, path::{Path, PathBuf}, }; struct Build { cfg: cc::Build, is_msvc: bool, } impl Build { fn new(cfg: cc::Build) -> Self { let is_msvc = cfg.try_get_compiler().unwrap().is_like_msvc(); Self { cfg, is_msvc } } fn append(&mut self, root: Option<&str>, files: &[&str]) { let root = root.map_or(String::new(), |s| { assert!(!s.ends_with('/'), "remove trailing slash"); format!("{s}/") }); self.cfg.files( files .into_iter() .map(|fname| format!("src/zlib-ng/{root}{fname}.c")), ); } fn mflag( &mut self, non_msvc: impl Into>, msvc: impl Into>, ) { let Some(flag) = (if self.is_msvc { msvc.into() } else { non_msvc.into() }) else { return; }; self.cfg.flag(flag); } } impl std::ops::Deref for Build { type Target = cc::Build; fn deref(&self) -> &Self::Target { &self.cfg } } impl std::ops::DerefMut for Build { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cfg } } /// Replicate the behavior of cmake/make/configure of stripping out the /// @ZLIB_SYMBOL_PREFIX@ since we don't want or need it fn strip_symbol_prefix(input: &Path, output: &Path, get_version: bool) -> String { let contents = fs::read_to_string(input) .map_err(|err| format!("failed to read {input:?}: {err}")) .unwrap(); let mut h = std::io::BufWriter::new(fs::File::create(output).expect("failed to create zlib include")); use std::io::IoSlice; let mut write = |bufs: &[IoSlice]| { // write_all_vectored is unstable for buf in bufs { h.write_all(&buf).unwrap(); } }; let mut version = None; for line in contents.lines() { if let Some((begin, end)) = line.split_once("@ZLIB_SYMBOL_PREFIX@") { write(&[ IoSlice::new(begin.as_bytes()), IoSlice::new(end.as_bytes()), IoSlice::new(b"\n"), ]); } else { write(&[IoSlice::new(line.as_bytes()), IoSlice::new(b"\n")]); } if get_version { if line.contains("ZLIBNG_VERSION") && line.contains("#define") { version = Some(line.split('"').nth(1).unwrap().to_owned()); } } } if get_version { version.expect("failed to detect ZLIBNG_VERSION") } else { String::new() } } pub fn build_zlib_ng(target: &str, compat: bool) { let mut cfg = cc::Build::new(); let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); let lib = dst.join("lib"); cfg.warnings(false).out_dir(&lib); let mut cfg = Build::new(cfg); cfg.append( None, &[ "adler32", "adler32_fold", "chunkset", "compare256", "compress", "cpu_features", "crc32_braid", "crc32_braid_comb", "crc32_fold", "deflate", "deflate_fast", "deflate_huff", "deflate_medium", "deflate_quick", "deflate_rle", "deflate_slow", "deflate_stored", "functable", // GZFILEOP "gzlib", "gzwrite", "infback", "inflate", "inftrees", "insert_string", "insert_string_roll", "slide_hash", "trees", "uncompr", "zutil", ], ); if compat { cfg.define("ZLIB_COMPAT", None); } cfg.define("WITH_GZFILEOP", None); { let mut build = dst.join("build"); fs::create_dir_all(&build).unwrap(); build.push("gzread.c"); strip_symbol_prefix(Path::new("src/zlib-ng/gzread.c.in"), &build, false); cfg.file(build); } let msvc = target.ends_with("pc-windows-msvc"); cfg.std("c11"); // This can be made configurable if it is an issue but most of these would // only fail if the user was on a decade old+ libc impl if !msvc { cfg.define("HAVE_ALIGNED_ALLOC", None) .define("HAVE_ATTRIBUTE_ALIGNED", None) .define("HAVE_BUILTIN_CTZ", None) .define("HAVE_BUILTIN_CTZLL", None) .define("HAVE_THREAD_LOCAL", None) .define("HAVE_VISIBILITY_HIDDEN", None) .define("HAVE_VISIBILITY_INTERNAL", None) .define("_LARGEFILE64_SOURCE", "1") .define("__USE_LARGEFILE64", None); // Turn implicit functions into errors, this would indicate eg. a // define is not set cfg.flag("-Werror-implicit-function-declaration"); } if !target.contains("windows") { cfg.define("STDC", None) .define("_POSIX_SOURCE", None) .define("HAVE_POSIX_MEMALIGN", None) .flag("-fvisibility=hidden"); } if target.contains("apple") { cfg.define("_C99_SOURCE", None); } else if target.contains("solaris") { cfg.define("_XOPEN_SOURCE", "700"); } let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("failed to retrieve target arch"); let features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap(); let is_linux_or_android = matches!(target_os.as_str(), "linux" | "android"); if is_linux_or_android { cfg.define("HAVE_SYS_AUXV_H", None); } match arch.as_str() { "x86_64" | "i686" => { cfg.define("X86_FEATURES", None); cfg.file("src/zlib-ng/arch/x86/x86_features.c"); let is_64 = arch.as_str() == "x86_64"; // AVX2 cfg.define("X86_AVX2", None); cfg.append( Some("arch/x86"), &[ "chunkset_avx2", "compare256_avx2", "adler32_avx2", "slide_hash_avx2", ], ); cfg.mflag("-mavx2", "/arch:AVX2"); // SSE2 cfg.define("X86_SSE2", None); cfg.append( Some("arch/x86"), &["chunkset_sse2", "compare256_sse2", "slide_hash_sse2"], ); cfg.mflag("-msse2", (!is_64).then_some("/arch:SSE2")); // SSE3 cfg.define("X86_SSSE3", None); cfg.append(Some("arch/x86"), &["adler32_ssse3", "chunkset_ssse3"]); cfg.mflag("-msse3", "/arch:SSE3"); // SSE4.2 cfg.define("X86_SSE42", None); cfg.append(Some("arch/x86"), &["adler32_sse42", "insert_string_sse42"]); cfg.mflag("-msse4.2", "/arch:SSE4.2"); // AVX-512 { for def in &[ "X86_AVX512", "X86_MASK_INTRIN", "X86_AVX512VNNI", "X86_VPCLMULQDQ_CRC", ] { cfg.define(def, None); } cfg.append( Some("arch/x86"), &["adler32_avx512", "adler32_avx512_vnni", "crc32_vpclmulqdq"], ); if cfg.is_msvc { cfg.flag("/arch:AVX512"); } else { // The zlib-ng cmake scripts to check target features claim that GCC doesn't // generate good code unless mtune is set, not sure if this is still the // case, but we faithfully replicate it just in case for flag in &[ "-mavx512f", "-mavx512dq", "-mavx512bw", "-mavx512vl", "-mavx512vnni", "-mvpclmulqdq", "-mtune=cascadelake", ] { cfg.flag(flag); } } } // Misc cfg.define("X86_PCLMULQDQ_CRC", None); cfg.append(Some("arch/x86"), &["crc32_pclmulqdq"]); cfg.mflag("-mpclmul", None); cfg.mflag("-mxsave", None); } "aarch64" | "arm" => { let is_aarch64 = arch == "aarch64"; cfg.define("ARM_FEATURES", None); cfg.file("src/zlib-ng/arch/arm/arm_features.c"); // Support runtime detection on linux/android if is_linux_or_android { cfg.define("ARM_AUXV_HAS_CRC32", None); if !is_aarch64 { cfg.define("ARM_AUXV_HAS_NEON", None); } } // According to the cmake macro, MSVC is missing the crc32 intrinsic // for arm, don't know if that is still true though if !cfg.is_msvc || is_aarch64 { cfg.define("ARM_ACLE", None).define("HAVE_ARM_ACLE_H", None); cfg.append(Some("arch/arm"), &["crc32_acle", "insert_string_acle"]); // When targeting aarch64 we already need to specify +simd, so // we do that once later in this block if !is_aarch64 { cfg.mflag("-march=armv8-a+crc", None); cfg.define("ARM_ASM_HWCAP", None); } } // neon // Fix armv7-unknown-linux-musleabi and arm-unknown-linux-musleabi by only // passing in ARM_NEON if that target is enabled. if features.split(",").any(|name| name == "neon") { cfg.define("ARM_NEON", None); } // NOTE: These intrinsics were only added in gcc 9.4, which is _relatively_ // recent, and if the define is not set zlib-ng just provides its // own implements, so maybe in a couple of years this can be toggled on // if building with cc is merged it makes sense to put compiler intrinsic/header // probing in a separate crate that can then be used here to enable // those intrinsics if the compiler supports them // * vld1q_u16_x4 // * vld1q_u8_x4 // * vst1q_u16_x4 // cfg.define("ARM_NEON_HASLD4", None) if cfg.is_msvc { cfg.define("__ARM_NEON__", None); } cfg.append( Some("arch/arm"), &[ "adler32_neon", "chunkset_neon", "compare256_neon", "slide_hash_neon", ], ); cfg.mflag( if is_aarch64 { "-march=armv8-a+crc+simd" } else { "-mfpu=neon" }, None, ); } "s390x" => { for def in &[ "S390_FEATURES", "S390_DFLTCC_DEFLATE", "S390_DFLTCC_INFLATE", "S390_CRC32_VX", ] { cfg.define(def, None); } cfg.flag("-DDFLTCC_LEVEL_MASK=0x7e"); cfg.append( Some("arch/s390"), &[ "crc32-vx", "dfltcc_common", "dfltcc_deflate", "dfltcc_inflate", "s390_features", ], ); } _ => { // NOTE: PowerPC and Riscv // zlib-ng can use intrinsics for both of these targets, however neither // of them are currently checked in CI, they will still work without // using the intrinsics, they will just be slower // PowerPC - // Riscv - } } let include = dst.join("include"); fs::create_dir_all(&include).unwrap(); let (zconf_h, zlib_h, mangle) = if compat { ("zconf.h", "zlib.h", "zlib_name_mangling.h") } else { fs::copy("src/zlib-ng/zconf-ng.h.in", include.join("zconf-ng.h")).unwrap(); ("zconf-ng.h", "zlib-ng.h", "zlib_name_mangling-ng.h") }; if msvc { fs::copy(format!("src/zlib-ng/{zconf_h}.in"), include.join(zconf_h)).unwrap(); } else { // If we don't do this then _some_ 32-bit targets will have an incorrect // size for off_t if they don't _also_ define `HAVE_UNISTD_H`, so we // copy configure/cmake here let new_zconf = fs::read_to_string(format!("src/zlib-ng/{zconf_h}.in")) .expect("failed to read zconf.h.in") .replace( "#ifdef HAVE_UNISTD_H /* may be set to #if 1 by configure/cmake/etc */", &format!( "#if 1 /* was set to #if 1 by {}:{}:{} */", file!(), line!(), column!() ), ); fs::write(include.join(zconf_h), new_zconf).unwrap(); } fs::copy( "src/zlib-ng/zlib_name_mangling.h.empty", include.join(mangle), ) .unwrap(); let version = strip_symbol_prefix( Path::new(&format!("src/zlib-ng/{zlib_h}.in")), &include.join(zlib_h), true, ); cfg.include(&include).include("src/zlib-ng"); if let Err(err) = cfg.try_compile("z") { let version = if !cfg.is_msvc { match std::process::Command::new(cfg.get_compiler().path()) .arg("--version") .output() { Ok(output) => String::from_utf8_lossy(&output.stdout).into_owned(), Err(_err) => "unknown".into(), } } else { "msvc".into() }; eprintln!("{err}"); panic!( "failed to compile zlib-ng with cc: detected compiler version as \n---\n{}---", version ); } fs::create_dir_all(lib.join("pkgconfig")).unwrap(); fs::write( lib.join("pkgconfig/zlib.pc"), fs::read_to_string("src/zlib-ng/zlib.pc.in") .unwrap() .replace("@prefix@", dst.to_str().unwrap()) .replace("@includedir@", "${prefix}/include") .replace("@libdir@", "${prefix}/lib") .replace("@VERSION@", &version), ) .unwrap(); println!("cargo:root={}", dst.display()); println!("cargo:rustc-link-search=native={}", lib.display()); println!("cargo:include={}", include.display()); if !compat { println!("cargo:rustc-cfg=zng"); } } #[allow(dead_code)] fn main() { let target = env::var("TARGET").unwrap(); build_zlib_ng(&target, false); }