blob: 09874e2d19ac707a74321bf33b380e5146332d70 [file] [log] [blame] [edit]
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<Option<&'static str>>,
msvc: impl Into<Option<&'static str>>,
) {
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 - <github issue here>
// Riscv - <github issue here>
}
}
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);
}