diff --git a/.gitignore b/.gitignore index c80746c73..d69413c15 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ install/ # Build artefacts /rust/target/ +/rust/cli/target/ contrib/linux-kernel/linux/ projects/ bin/ diff --git a/programs/Makefile b/programs/Makefile index 14f295c5b..bcd268e99 100644 --- a/programs/Makefile +++ b/programs/Makefile @@ -32,7 +32,8 @@ RUST_CLI_MANIFEST := $(RUST_CLI_DIR)/Cargo.toml RUST_SOURCES := $(RUST_MANIFEST) $(RUST_DIR)/Cargo.lock \ $(shell find $(RUST_DIR)/src -type f -name '*.rs' -print) RUST_CLI_SOURCES := $(RUST_CLI_MANIFEST) $(RUST_CLI_DIR)/Cargo.lock \ - $(RUST_CLI_DIR)/src/lib.rs $(RUST_DIR)/src/zstd_cli.rs + $(RUST_CLI_DIR)/src/lib.rs $(RUST_DIR)/src/zstd_cli.rs \ + $(RUST_DIR)/src/timefn.rs # Keep Rust's HUF implementation in lockstep with libzstd.mk's C selection. # Forced modes may arrive as libzstd.mk variables or as direct -D flags in @@ -88,7 +89,7 @@ RUST_CLI_STATICLIB := $(RUST_CLI_TARGET_DIR)/release/libzstd_cli_rs.a RUST_CLI_STATICLIB_32 := $(RUST_CLI_TARGET_DIR)/$(RUST_TARGET_32)/release/libzstd_cli_rs.a RUST_CLI_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ --target-dir $(RUST_CLI_TARGET_DIR) \ - --no-default-features --features compression,decompression + --no-default-features --features cli,compression,decompression $(RUST_CLI_STATICLIB): $(RUST_CLI_SOURCES) $(CARGO) build $(RUST_CLI_CARGO_FLAGS) @@ -114,7 +115,7 @@ RUST_DECOMPRESS_CLI_TARGET_DIR := $(RUST_DIR)/target/$(RUST_DECOMPRESS_CLI_BUILD RUST_DECOMPRESS_CLI_STATICLIB := $(RUST_DECOMPRESS_CLI_TARGET_DIR)/release/libzstd_cli_rs.a RUST_DECOMPRESS_CLI_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ --target-dir $(RUST_DECOMPRESS_CLI_TARGET_DIR) \ - --no-default-features --features decompression + --no-default-features --features cli,decompression $(RUST_DECOMPRESS_CLI_STATICLIB): $(RUST_CLI_SOURCES) $(CARGO) build $(RUST_DECOMPRESS_CLI_CARGO_FLAGS) @@ -145,7 +146,7 @@ RUST_COMPRESS_CLI_TARGET_DIR := $(RUST_DIR)/target/$(RUST_COMPRESS_CLI_BUILD_CON RUST_COMPRESS_CLI_STATICLIB := $(RUST_COMPRESS_CLI_TARGET_DIR)/release/libzstd_cli_rs.a RUST_COMPRESS_CLI_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ --target-dir $(RUST_COMPRESS_CLI_TARGET_DIR) \ - --no-default-features --features compression + --no-default-features --features cli,compression $(RUST_COMPRESS_CLI_STATICLIB): $(RUST_CLI_SOURCES) $(CARGO) build $(RUST_COMPRESS_CLI_CARGO_FLAGS) diff --git a/programs/timefn.c b/programs/timefn.c index 4f045226b..349bd26dc 100644 --- a/programs/timefn.c +++ b/programs/timefn.c @@ -8,161 +8,14 @@ * You may select, at your option, one of the above-listed licenses. */ - -/* === Dependencies === */ +/* The implementation lives in rust/src/timefn.rs, built into the Rust CLI + * static archive. This translation unit stays in the original source lists so + * build configuration keeps working while the implementation is in Rust. */ #include "timefn.h" -#include "platform.h" /* set _POSIX_C_SOURCE */ -#include /* CLOCK_MONOTONIC, TIME_UTC */ -/*-**************************************** -* Time functions -******************************************/ - -#if defined(_WIN32) /* Windows */ - -#include /* LARGE_INTEGER */ -#include /* abort */ -#include /* perror */ - -UTIL_time_t UTIL_getTime(void) -{ - static LARGE_INTEGER ticksPerSecond; - static int init = 0; - if (!init) { - if (!QueryPerformanceFrequency(&ticksPerSecond)) { - perror("timefn::QueryPerformanceFrequency"); - abort(); - } - init = 1; - } - { UTIL_time_t r; - LARGE_INTEGER x; - QueryPerformanceCounter(&x); - r.t = (PTime)(x.QuadPart * 1000000000ULL / ticksPerSecond.QuadPart); - return r; - } -} - - -#elif defined(__APPLE__) && defined(__MACH__) - -#include /* mach_timebase_info_data_t, mach_timebase_info, mach_absolute_time */ - -UTIL_time_t UTIL_getTime(void) -{ - static mach_timebase_info_data_t rate; - static int init = 0; - if (!init) { - mach_timebase_info(&rate); - init = 1; - } - { UTIL_time_t r; - r.t = mach_absolute_time() * (PTime)rate.numer / (PTime)rate.denom; - return r; - } -} - -/* POSIX.1-2001 (optional) */ -#elif defined(CLOCK_MONOTONIC) - -#include /* abort */ -#include /* perror */ - -UTIL_time_t UTIL_getTime(void) -{ - /* time must be initialized, othersize it may fail msan test. - * No good reason, likely a limitation of timespec_get() for some target */ - struct timespec time = { 0, 0 }; - if (clock_gettime(CLOCK_MONOTONIC, &time) != 0) { - perror("timefn::clock_gettime(CLOCK_MONOTONIC)"); - abort(); - } - { UTIL_time_t r; - r.t = (PTime)time.tv_sec * 1000000000ULL + (PTime)time.tv_nsec; - return r; - } -} - - -/* C11 requires support of timespec_get(). - * However, FreeBSD 11 claims C11 compliance while lacking timespec_get(). - * Double confirm timespec_get() support by checking the definition of TIME_UTC. - * However, some versions of Android manage to simultaneously define TIME_UTC - * and lack timespec_get() support... */ -#elif (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* C11 */) \ - && defined(TIME_UTC) && !defined(__ANDROID__) - -#include /* abort */ -#include /* perror */ - -UTIL_time_t UTIL_getTime(void) -{ - /* time must be initialized, othersize it may fail msan test. - * No good reason, likely a limitation of timespec_get() for some target */ - struct timespec time = { 0, 0 }; - if (timespec_get(&time, TIME_UTC) != TIME_UTC) { - perror("timefn::timespec_get(TIME_UTC)"); - abort(); - } - { UTIL_time_t r; - r.t = (PTime)time.tv_sec * 1000000000ULL + (PTime)time.tv_nsec; - return r; - } -} - - -#else /* relies on standard C90 (note : clock_t produces wrong measurements for multi-threaded workloads) */ - -UTIL_time_t UTIL_getTime(void) -{ - UTIL_time_t r; - r.t = (PTime)clock() * 1000000000ULL / CLOCKS_PER_SEC; - return r; -} - -#define TIME_MT_MEASUREMENTS_NOT_SUPPORTED - -#endif - -/* ==== Common functions, valid for all time API ==== */ - -PTime UTIL_getSpanTimeNano(UTIL_time_t clockStart, UTIL_time_t clockEnd) -{ - return clockEnd.t - clockStart.t; -} - -PTime UTIL_getSpanTimeMicro(UTIL_time_t begin, UTIL_time_t end) -{ - return UTIL_getSpanTimeNano(begin, end) / 1000ULL; -} - -PTime UTIL_clockSpanMicro(UTIL_time_t clockStart ) -{ - UTIL_time_t const clockEnd = UTIL_getTime(); - return UTIL_getSpanTimeMicro(clockStart, clockEnd); -} - -PTime UTIL_clockSpanNano(UTIL_time_t clockStart ) -{ - UTIL_time_t const clockEnd = UTIL_getTime(); - return UTIL_getSpanTimeNano(clockStart, clockEnd); -} - -void UTIL_waitForNextTick(void) -{ - UTIL_time_t const clockStart = UTIL_getTime(); - UTIL_time_t clockEnd; - do { - clockEnd = UTIL_getTime(); - } while (UTIL_getSpanTimeNano(clockStart, clockEnd) == 0); -} - -int UTIL_support_MT_measurements(void) -{ -# if defined(TIME_MT_MEASUREMENTS_NOT_SUPPORTED) - return 0; -# else - return 1; -# endif -} +/* The Rust port mirrors this exact ABI: UTIL_time_t is returned by value and + * must remain a plain 64-bit nanosecond counter. */ +typedef char UTIL_staticAssert_ptimeIs64Bit[(sizeof(PTime) == 8) ? 1 : -1]; +typedef char UTIL_staticAssert_timeIsPlainCounter[ + (sizeof(UTIL_time_t) == sizeof(PTime)) ? 1 : -1]; diff --git a/rust/README.md b/rust/README.md index 0dc056c7e..ff4502bcc 100644 --- a/rust/README.md +++ b/rust/README.md @@ -69,6 +69,11 @@ zstd ABI: so library builds do not acquire program-only dependencies. The C `fileio` backend still owns file opening, safe replacement, sparse writes, metadata, and streaming I/O. + - `timefn` provides the monotonic nanosecond clock behind `UTIL_time_t`. + It also lives in the `cli/` package, but C test binaries (fullbench, + fuzzer, zstreamtest, paramgrill, ...) link a helpers-only build of that + archive, produced without the package's `cli` feature, because the parser + layer requires the C `fileio` backend that tests do not compile. The optimal block matcher, high-level frame compression, dictionary-building except suffix-array construction, legacy decoding callbacks, and the CLI @@ -111,8 +116,9 @@ from `rust/cli` as well: ```sh cargo clippy --all-targets -- -D warnings cargo test --all-targets -cargo test --no-default-features --features compression --all-targets -cargo test --no-default-features --features decompression --all-targets +cargo test --no-default-features --features cli,compression --all-targets +cargo test --no-default-features --features cli,decompression --all-targets +cargo test --no-default-features --all-targets ``` Then run original compatibility tests from the repository root, starting with diff --git a/rust/cli/Cargo.lock b/rust/cli/Cargo.lock index dc58719bc..ab11ff18b 100644 --- a/rust/cli/Cargo.lock +++ b/rust/cli/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + [[package]] name = "zstd-cli-rs" version = "0.1.0" +dependencies = [ + "libc", +] diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml index 99830b3f5..d4c14a623 100644 --- a/rust/cli/Cargo.toml +++ b/rust/cli/Cargo.toml @@ -7,6 +7,13 @@ edition = "2021" crate-type = ["staticlib"] [features] -default = ["compression", "decompression"] +default = ["cli", "compression", "decompression"] +# The command-line parser and dispatch layer, which requires the C fileio +# backend at link time. Program archives enable it; C test binaries link a +# helpers-only archive (timefn) built without it. +cli = [] compression = [] decompression = [] + +[dependencies] +libc = "0.2" diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 458d3ef36..e33acb81b 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,2 +1,5 @@ +#[path = "../../src/timefn.rs"] +mod timefn; +#[cfg(feature = "cli")] #[path = "../../src/zstd_cli.rs"] mod zstd_cli; diff --git a/rust/src/timefn.rs b/rust/src/timefn.rs new file mode 100644 index 000000000..c6e97129a --- /dev/null +++ b/rust/src/timefn.rs @@ -0,0 +1,198 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +//! Precise monotonic time measurement for the command-line programs. +//! +//! Port of `programs/timefn.c`. `UTIL_time_t` is a plain nanosecond counter +//! whose absolute value is meaningless; only spans between two measurements +//! are valid. The struct crosses the C ABI by value, so it stays `repr(C)` +//! with the exact `timefn.h` layout. +//! +//! Platform selection mirrors the C preprocessor structure: Windows uses the +//! performance counter, Apple systems use the Mach absolute clock, and other +//! POSIX systems use `clock_gettime(CLOCK_MONOTONIC)`. The C90 `clock()` +//! fallback is never needed on targets Rust supports, so multi-threaded +//! measurements are always supported. + +use std::os::raw::c_int; + +/// Precise Time (`PTime` in timefn.h): an unsigned 64-bit nanosecond count. +pub type PTime = u64; + +/// Nanosecond time counter with the `timefn.h` `UTIL_time_t` layout. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct UTIL_time_t { + pub t: PTime, +} + +const _: () = assert!(std::mem::size_of::() == 8); +const _: () = assert!(std::mem::size_of::() == std::mem::size_of::()); + +#[cfg(windows)] +mod platform { + use super::PTime; + use std::sync::OnceLock; + + #[link(name = "kernel32")] + unsafe extern "system" { + /// Takes a `LARGE_INTEGER*`; the union is ABI-identical to `i64*`. + fn QueryPerformanceCounter(count: *mut i64) -> i32; + fn QueryPerformanceFrequency(frequency: *mut i64) -> i32; + } + + pub fn monotonic_ns() -> PTime { + static TICKS_PER_SECOND: OnceLock = OnceLock::new(); + let ticks_per_second = *TICKS_PER_SECOND.get_or_init(|| { + let mut frequency = 0i64; + if unsafe { QueryPerformanceFrequency(&mut frequency) } == 0 { + eprintln!( + "timefn::QueryPerformanceFrequency: {}", + std::io::Error::last_os_error() + ); + std::process::abort(); + } + frequency + }); + let mut counter = 0i64; + unsafe { QueryPerformanceCounter(&mut counter) }; + (counter as PTime).wrapping_mul(1_000_000_000) / ticks_per_second as PTime + } +} + +#[cfg(all(unix, target_vendor = "apple"))] +mod platform { + use super::PTime; + use std::sync::OnceLock; + + pub fn monotonic_ns() -> PTime { + static RATE: OnceLock<(PTime, PTime)> = OnceLock::new(); + let (numer, denom) = *RATE.get_or_init(|| { + let mut rate = libc::mach_timebase_info { numer: 0, denom: 0 }; + unsafe { libc::mach_timebase_info(&mut rate) }; + (PTime::from(rate.numer), PTime::from(rate.denom)) + }); + unsafe { libc::mach_absolute_time() }.wrapping_mul(numer) / denom + } +} + +#[cfg(all(unix, not(target_vendor = "apple")))] +mod platform { + use super::PTime; + + pub fn monotonic_ns() -> PTime { + // Zero-initialized like the C source, which works around timespec_get + // msan limitations on some targets. + let mut time: libc::timespec = unsafe { std::mem::zeroed() }; + if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } != 0 { + eprintln!( + "timefn::clock_gettime(CLOCK_MONOTONIC): {}", + std::io::Error::last_os_error() + ); + std::process::abort(); + } + (time.tv_sec as PTime) + .wrapping_mul(1_000_000_000) + .wrapping_add(time.tv_nsec as PTime) + } +} + +/// Returns the current value of the platform's monotonic nanosecond clock. +#[no_mangle] +pub extern "C" fn UTIL_getTime() -> UTIL_time_t { + UTIL_time_t { + t: platform::monotonic_ns(), + } +} + +/// Nanoseconds elapsed between two measurements, with C unsigned wrap-around. +#[no_mangle] +pub extern "C" fn UTIL_getSpanTimeNano(clockStart: UTIL_time_t, clockEnd: UTIL_time_t) -> PTime { + clockEnd.t.wrapping_sub(clockStart.t) +} + +/// Microseconds elapsed between two measurements, truncated like C division. +#[no_mangle] +pub extern "C" fn UTIL_getSpanTimeMicro(begin: UTIL_time_t, end: UTIL_time_t) -> PTime { + UTIL_getSpanTimeNano(begin, end) / 1000 +} + +/// Microseconds elapsed since `clockStart`. +#[no_mangle] +pub extern "C" fn UTIL_clockSpanMicro(clockStart: UTIL_time_t) -> PTime { + UTIL_getSpanTimeMicro(clockStart, UTIL_getTime()) +} + +/// Nanoseconds elapsed since `clockStart`. +#[no_mangle] +pub extern "C" fn UTIL_clockSpanNano(clockStart: UTIL_time_t) -> PTime { + UTIL_getSpanTimeNano(clockStart, UTIL_getTime()) +} + +/// Busy-waits until the clock produces a new tick, improving measurement +/// accuracy on platforms with a low timer resolution. +#[no_mangle] +pub extern "C" fn UTIL_waitForNextTick() { + let clockStart = UTIL_getTime(); + loop { + let clockEnd = UTIL_getTime(); + if UTIL_getSpanTimeNano(clockStart, clockEnd) != 0 { + return; + } + } +} + +/// All clock sources used by the Rust port are valid under multi-threaded +/// workloads; only the C90 `clock()` fallback of the C source was not. +#[no_mangle] +pub extern "C" fn UTIL_support_MT_measurements() -> c_int { + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nanosecond_spans_subtract_with_unsigned_wrap_around() { + let start = UTIL_time_t { t: 100 }; + let end = UTIL_time_t { t: 350 }; + + assert_eq!(UTIL_getSpanTimeNano(start, end), 250); + assert_eq!(UTIL_getSpanTimeNano(end, start), u64::MAX - 249); + assert_eq!(UTIL_getSpanTimeNano(start, start), 0); + } + + #[test] + fn microsecond_spans_truncate_sub_tick_remainders() { + let start = UTIL_time_t { t: 0 }; + + assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 999 }), 0); + assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 1_000 }), 1); + assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 1_999 }), 1); + assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 2_000 }), 2); + } + + #[test] + fn clock_is_monotonic_across_measurements() { + let first = UTIL_getTime(); + let second = UTIL_getTime(); + + assert!(second.t >= first.t); + assert!(UTIL_clockSpanNano(first) >= UTIL_getSpanTimeNano(first, second)); + } + + #[test] + fn waiting_for_the_next_tick_advances_the_clock() { + let before = UTIL_getTime(); + UTIL_waitForNextTick(); + let after = UTIL_getTime(); + + assert!(UTIL_getSpanTimeNano(before, after) > 0); + } + + #[test] + fn multi_threaded_measurements_are_supported() { + assert_eq!(UTIL_support_MT_measurements(), 1); + } +} diff --git a/tests/Makefile b/tests/Makefile index e986fb237..15c1f1cdf 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -93,6 +93,28 @@ $(RUST_STATICLIB): $(RUST_SOURCES) $(RUST_STATICLIB_32): $(RUST_SOURCES) $(CARGO) build $(RUST_CARGO_FLAGS) --target $(RUST_TARGET_32) +# Program-only helpers that were C sources shared with the tests (timefn, +# benchfn) now live in the Rust CLI package. The tests link a helpers-only +# archive, built without the `cli` feature: the parser/dispatch layer needs +# the C fileio backend, which test binaries do not provide. +RUST_CLI_DIR := $(RUST_DIR)/cli +RUST_CLI_MANIFEST := $(RUST_CLI_DIR)/Cargo.toml +RUST_CLI_HELPER_SOURCES := $(RUST_CLI_MANIFEST) $(RUST_CLI_DIR)/Cargo.lock \ + $(RUST_CLI_DIR)/src/lib.rs \ + $(RUST_DIR)/src/timefn.rs +RUST_CLI_HELPERS_TARGET_DIR := $(RUST_DIR)/target/cli-helpers +RUST_CLI_HELPERS_STATICLIB := $(RUST_CLI_HELPERS_TARGET_DIR)/release/libzstd_cli_rs.a +RUST_CLI_HELPERS_STATICLIB_32 := $(RUST_CLI_HELPERS_TARGET_DIR)/$(RUST_TARGET_32)/release/libzstd_cli_rs.a +RUST_CLI_HELPERS_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ + --target-dir $(RUST_CLI_HELPERS_TARGET_DIR) \ + --no-default-features + +$(RUST_CLI_HELPERS_STATICLIB): $(RUST_CLI_HELPER_SOURCES) + $(CARGO) build $(RUST_CLI_HELPERS_CARGO_FLAGS) + +$(RUST_CLI_HELPERS_STATICLIB_32): $(RUST_CLI_HELPER_SOURCES) + $(CARGO) build $(RUST_CLI_HELPERS_CARGO_FLAGS) --target $(RUST_TARGET_32) + # These test objects have flat filenames, unlike the configuration-hashed # program objects. Track the HUF mode separately so a C object set compiled # for one decoder is never relinked with a Rust archive for another decoder. @@ -252,8 +274,8 @@ fuzzer32 : $(ZSTD_FILES) $(LINK.c) $^ -o $@$(EXT) # note : broken : requires symbols unavailable from dynamic library -fuzzer-dll : $(LIB_SRCDIR)/common/xxhash.c $(PRGDIR)/util.c $(PRGDIR)/timefn.c $(PRGDIR)/datagen.c fuzzer.c - $(CC) $(CPPFLAGS) $(CFLAGS) $(filter %.c,$^) $(LDFLAGS) -o $@$(EXT) +fuzzer-dll : $(LIB_SRCDIR)/common/xxhash.c $(PRGDIR)/util.c $(PRGDIR)/timefn.c $(PRGDIR)/datagen.c fuzzer.c $(RUST_CLI_HELPERS_STATICLIB) + $(CC) $(CPPFLAGS) $(CFLAGS) $(filter %.c,$^) $(RUST_CLI_HELPERS_STATICLIB) $(LDFLAGS) -o $@$(EXT) CLEAN += zstreamtest zstreamtest32 ZSTREAM_LOCAL_FILES := $(PRGDIR)/datagen.c $(PRGDIR)/util.c $(PRGDIR)/timefn.c seqgen.c zstreamtest.c external_matchfinder.c @@ -284,8 +306,8 @@ zstreamtest_ubsan : $(ZSTREAMFILES) # note : broken : requires symbols unavailable from dynamic library zstreamtest-dll : $(LIB_SRCDIR)/common/xxhash.c # xxh symbols not exposed from dll -zstreamtest-dll : $(ZSTREAM_LOCAL_FILES) - $(CC) $(CPPFLAGS) $(CFLAGS) $(filter %.c,$^) $(LDFLAGS) -o $@$(EXT) +zstreamtest-dll : $(ZSTREAM_LOCAL_FILES) $(RUST_CLI_HELPERS_STATICLIB) + $(CC) $(CPPFLAGS) $(CFLAGS) $(filter %.c,$^) $(RUST_CLI_HELPERS_STATICLIB) $(LDFLAGS) -o $@$(EXT) CLEAN += paramgrill paramgrill : DEBUGFLAGS = # turn off debug for speed measurements @@ -344,6 +366,17 @@ $(RUST_LINK_TARGETS_32): $(RUST_STATICLIB_32) $(RUST_LINK_TARGETS) $(RUST_LINK_TARGETS_32): $(RUST_HUF_C_MODE_STAMP) +# Tests that compile the timefn/benchfn C shims also link the Rust CLI +# helpers archive, which owns those implementations. The archive is a +# prerequisite so `$^` places it after every C object referencing its symbols. +RUST_CLI_LINK_TARGETS := fullbench fullbench-lib fullbench-dll fuzzer \ + zstreamtest zstreamtest_asan zstreamtest_tsan \ + zstreamtest_ubsan paramgrill decodecorpus poolTests +$(RUST_CLI_LINK_TARGETS): $(RUST_CLI_HELPERS_STATICLIB) + +RUST_CLI_LINK_TARGETS_32 := fullbench32 fuzzer32 zstreamtest32 +$(RUST_CLI_LINK_TARGETS_32): $(RUST_CLI_HELPERS_STATICLIB_32) + .PHONY: versionsTest versionsTest: clean $(PYTHON) test-zstd-versions.py