use std::env; fn defined(var: &str) -> bool { println!("cargo:rerun-if-env-changed={}", var); env::var_os(var).is_some() } fn is_pure() -> bool { defined("CARGO_FEATURE_PURE") } fn should_prefer_intrinsics() -> bool { defined("CARGO_FEATURE_PREFER_INTRINSICS") } fn is_neon() -> bool { defined("CARGO_FEATURE_NEON") } fn is_no_neon() -> bool { defined("CARGO_FEATURE_NO_NEON") } fn is_ci() -> bool { defined("BLAKE3_CI") } fn warn(warning: &str) { assert!(!warning.contains("\n")); println!("cargo:warning={}", warning); if is_ci() { println!("cargo:warning=Warnings in CI are treated as errors. Build failed."); std::process::exit(1); } } fn target_components() -> Vec { let target = env::var("TARGET").unwrap(); target.split("-").map(|s| s.to_string()).collect() } fn is_x86_64() -> bool { target_components()[0] == "x86_64" } fn is_x86_32() -> bool { let arch = &target_components()[0]; arch == "i386" || arch == "i586" || arch == "i686" } fn is_arm() -> bool { is_armv7() || is_aarch64() || target_components()[0] == "arm" } fn is_aarch64() -> bool { target_components()[0] == "aarch64" } fn is_armv7() -> bool { target_components()[0] == "armv7" } fn endianness() -> String { let endianness = env::var("CARGO_CFG_TARGET_ENDIAN").unwrap(); assert!(endianness == "little" || endianness == "big"); endianness } fn is_little_endian() -> bool { endianness() == "little" } fn is_big_endian() -> bool { endianness() == "big" } // Windows targets may be using the MSVC toolchain or the MinGW toolchain. The // right compiler flags to use depend on the toolchain. (And we don't want to // use flag_if_supported, because we don't want features to be silently // disabled by old compilers.) fn is_windows_msvc() -> bool { // Some targets are only two components long, so check in steps. target_components()[1] == "pc" && target_components()[2] == "windows" && target_components()[3] == "msvc" } // MinGW toolchain uses 2 different targets depending on the main compiler. // Target for a general MinGW toolchain ends with `-gnu` (GCC is used as C // compiler). Target for a LLVM-MinGW toolchain (Clang is used as C compiler) // ends with `-gnullvm`. fn is_windows_gnu() -> bool { // Some targets are only two components long, so check in steps. target_components()[1] == "pc" && target_components()[2] == "windows" && target_components()[3] != "msvc" } fn new_build() -> cc::Build { let mut build = cc::Build::new(); if !is_windows_msvc() { build.flag("-std=c11"); } // Do NOT trigger a rebuild any time the env changes (e.g. $PATH). // This prevents all downstream crates from being rebuilt when `cargo check` // or `cargo build` are run in different environments, like Rust Analyzer // vs. in the terminal vs. in a Git pre-commit hook. build.emit_rerun_if_env_changed(false); build } #[derive(PartialEq)] enum CCompilerSupport { NoCompiler, NoAVX512, YesAVX512, } use CCompilerSupport::*; fn c_compiler_support() -> CCompilerSupport { let build = new_build(); let flags_checked; let support_result: Result = if is_windows_msvc() { flags_checked = "/arch:AVX512"; build.is_flag_supported("/arch:AVX512") } else { // Check for both of the flags we use. If -mavx512f works, then -mavx512vl // will probably always work too, but we might as well be thorough. flags_checked = "-mavx512f and -mavx512vl"; match build.is_flag_supported("-mavx512f") { Ok(true) => build.is_flag_supported("-mavx512vl"), false_or_error => false_or_error, } }; match support_result { Ok(true) => YesAVX512, Ok(false) => { warn(&format!( "The C compiler {:?} does not support {}.", build.get_compiler().path(), flags_checked, )); NoAVX512 } Err(e) => { println!("{:?}", e); warn(&format!( "No C compiler {:?} detected.", build.get_compiler().path() )); NoCompiler } } } fn build_sse2_sse41_avx2_rust_intrinsics() { // No C code to compile here. Set the cfg flags that enable the Rust SSE2, // SSE4.1, and AVX2 intrinsics modules. The regular Cargo build will compile // them. println!("cargo:rustc-cfg=blake3_sse2_rust"); println!("cargo:rustc-cfg=blake3_sse41_rust"); println!("cargo:rustc-cfg=blake3_avx2_rust"); } fn build_sse2_sse41_avx2_assembly() { // Build the assembly implementations for SSE4.1 and AVX2. This is // preferred, but it only supports x86_64. assert!(is_x86_64()); println!("cargo:rustc-cfg=blake3_sse2_ffi"); println!("cargo:rustc-cfg=blake3_sse41_ffi"); println!("cargo:rustc-cfg=blake3_avx2_ffi"); let mut build = new_build(); if is_windows_msvc() { build.file("c/blake3_sse2_x86-64_windows_msvc.asm"); build.file("c/blake3_sse41_x86-64_windows_msvc.asm"); build.file("c/blake3_avx2_x86-64_windows_msvc.asm"); } else if is_windows_gnu() { build.file("c/blake3_sse2_x86-64_windows_gnu.S"); build.file("c/blake3_sse41_x86-64_windows_gnu.S"); build.file("c/blake3_avx2_x86-64_windows_gnu.S"); } else { // All non-Windows implementations are assumed to support // Linux-style assembly. These files do contain a small // explicit workaround for macOS also. build.file("c/blake3_sse2_x86-64_unix.S"); build.file("c/blake3_sse41_x86-64_unix.S"); build.file("c/blake3_avx2_x86-64_unix.S"); } build.compile("blake3_sse2_sse41_avx2_assembly"); } fn build_avx512_c_intrinsics() { // This is required on 32-bit x86 targets, since the assembly // implementation doesn't support those. println!("cargo:rustc-cfg=blake3_avx512_ffi"); let mut build = new_build(); build.file("c/blake3_avx512.c"); if is_windows_msvc() { build.flag("/arch:AVX512"); } else { build.flag("-mavx512f"); build.flag("-mavx512vl"); } if is_windows_gnu() { // Workaround for https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65782. build.flag("-fno-asynchronous-unwind-tables"); } build.compile("blake3_avx512_intrinsics"); } fn build_avx512_assembly() { // Build the assembly implementation for AVX-512. This is preferred, but it // only supports x86_64. assert!(is_x86_64()); println!("cargo:rustc-cfg=blake3_avx512_ffi"); let mut build = new_build(); if is_windows_msvc() { build.file("c/blake3_avx512_x86-64_windows_msvc.asm"); } else { if is_windows_gnu() { build.file("c/blake3_avx512_x86-64_windows_gnu.S"); } else { // All non-Windows implementations are assumed to support Linux-style // assembly. These files do contain a small explicit workaround for // macOS also. build.file("c/blake3_avx512_x86-64_unix.S"); } // Older versions of Clang require these flags, even for assembly. See // https://github.com/BLAKE3-team/BLAKE3/issues/79. build.flag("-mavx512f"); build.flag("-mavx512vl"); } build.compile("blake3_avx512_assembly"); } fn build_neon_c_intrinsics() { let mut build = new_build(); // Note that blake3_neon.c normally depends on the blake3_portable.c // for the single-instance compression function, but we expose // portable.rs over FFI instead. See ffi_neon.rs. build.file("c/blake3_neon.c"); // ARMv7 platforms that support NEON generally need the following // flags. AArch64 supports NEON by default and does not support -mpfu. if is_armv7() { build.flag("-mfpu=neon-vfpv4"); build.flag("-mfloat-abi=hard"); } build.compile("blake3_neon"); } fn main() -> Result<(), Box> { // As of Rust 1.80, unrecognized config names are warnings. Give Cargo all of our config names. let all_cfgs = [ "blake3_sse2_ffi", "blake3_sse2_rust", "blake3_sse41_ffi", "blake3_sse41_rust", "blake3_avx2_ffi", "blake3_avx2_rust", "blake3_avx512_ffi", "blake3_neon", ]; for cfg_name in all_cfgs { // TODO: Switch this whole file to the new :: syntax when our MSRV reaches 1.77. // https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script println!("cargo:rustc-check-cfg=cfg({cfg_name}, values(none()))"); } if is_pure() && is_neon() { panic!("It doesn't make sense to enable both \"pure\" and \"neon\"."); } if is_no_neon() && is_neon() { panic!("It doesn't make sense to enable both \"no_neon\" and \"neon\"."); } if is_x86_64() || is_x86_32() { let support = c_compiler_support(); if is_x86_32() || should_prefer_intrinsics() || is_pure() || support == NoCompiler { build_sse2_sse41_avx2_rust_intrinsics(); } else { // We assume that all C compilers can assemble SSE4.1 and AVX2. We // don't explicitly check for support. build_sse2_sse41_avx2_assembly(); } if is_pure() || support == NoCompiler || support == NoAVX512 { // The binary will not include any AVX-512 code. } else if is_x86_32() || should_prefer_intrinsics() { build_avx512_c_intrinsics(); } else { build_avx512_assembly(); } } if is_neon() && is_big_endian() { panic!("The NEON implementation doesn't support big-endian ARM.") } if (is_arm() && is_neon()) || (!is_no_neon() && !is_pure() && is_aarch64() && is_little_endian()) { println!("cargo:rustc-cfg=blake3_neon"); build_neon_c_intrinsics(); } // The `cc` crate doesn't automatically emit rerun-if directives for the // environment variables it supports, in particular for $CC. We expect to // do a lot of benchmarking across different compilers, so we explicitly // add the variables that we're likely to need. println!("cargo:rerun-if-env-changed=CC"); println!("cargo:rerun-if-env-changed=CFLAGS"); // Ditto for source files, though these shouldn't change as often. for file in std::fs::read_dir("c")? { println!( "cargo:rerun-if-changed={}", file?.path().to_str().expect("utf-8") ); } Ok(()) }