Skip to content

Commit 9abb582

Browse files
committed
add cpu detection module and richer --version output
- New src/cpu.rs: runtime CPU feature detection (AVX2/AVX-512/SSE4.2/POPCNT on x86_64, SVE/SVE2/NEON on aarch64), upgrade hint when a faster variant is available, and check_cpu_compat() pre-parse guard so SIMD-tuned builds fail friendly rather than SIGILL on an unsupported CPU. - build.rs now derives OS + SIMD level from CARGO_CFG_TARGET_* and emits a pre-formatted VERSION_BODY env var consumed directly by clap's long_version and by the startup banner. - --version now reports two lines: "ruSTAR X.Y.Z\n<hash> - <os>/<target> (<label>) - built <ts>". - Startup banner logs version, build detail, detected CPU features, and an upgrade hint (when relevant) — the hint is deliberately only in the run log, not in --version.
1 parent c0892fb commit 9abb582

5 files changed

Lines changed: 242 additions & 10 deletions

File tree

build.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
//! Build script — embeds git/build metadata as compile-time environment
2-
//! variables so the binary can report exactly which commit it was built from.
2+
//! variables so the binary can report exactly which commit and target it
3+
//! was built from.
34
//!
45
//! Embedded variables:
5-
//! - `GIT_SHORT_HASH` — short commit hash, or `unknown`
6-
//! - `BUILD_TIMESTAMP` — UTC timestamp of the build (ISO-8601)
6+
//! - `GIT_SHORT_HASH` — short commit hash, or `unknown`
7+
//! - `BUILD_TIMESTAMP` — UTC timestamp of the build (ISO-8601)
8+
//! - `VERSION_BODY` — pre-formatted `--version` detail line, e.g.
9+
//! `c0892fb - linux/aarch64 (NEON) - built 2026-04-16T21:36:44Z`
710
811
use std::process::Command;
912

@@ -27,6 +30,42 @@ fn main() {
2730
let build_ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
2831
println!("cargo:rustc-env=BUILD_TIMESTAMP={build_ts}");
2932

33+
// Derive the SIMD level and label from the TARGET being compiled for,
34+
// using the `CARGO_CFG_*` env vars Cargo exposes to build scripts.
35+
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
36+
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
37+
let target_features = std::env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
38+
let has_feature = |f: &str| target_features.split(',').any(|x| x == f);
39+
40+
let (target, label) = match target_arch.as_str() {
41+
"x86_64" => {
42+
if has_feature("avx512f") {
43+
("x86-64-v4", "AVX-512")
44+
} else if has_feature("avx2") {
45+
("x86-64-v3", "AVX2")
46+
} else {
47+
("x86-64", "baseline")
48+
}
49+
}
50+
"aarch64" => {
51+
if has_feature("sve") {
52+
("aarch64-neoverse-v1", "SVE")
53+
} else {
54+
("aarch64", "NEON")
55+
}
56+
}
57+
_ => ("unknown", "baseline"),
58+
};
59+
60+
let os = match target_os.as_str() {
61+
"linux" | "macos" | "windows" => target_os.as_str(),
62+
_ => "unknown",
63+
};
64+
65+
println!(
66+
"cargo:rustc-env=VERSION_BODY={git_hash} - {os}/{target} ({label}) - built {build_ts}"
67+
);
68+
3069
println!("cargo:rerun-if-changed=.git/HEAD");
3170
println!("cargo:rerun-if-env-changed=GIT_SHORT_HASH");
3271
}

src/cpu.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//! Runtime CPU feature detection and compatibility guard for SIMD-tuned
2+
//! binaries. The compile-time build flavour (OS, arch, SIMD level, git hash,
3+
//! build timestamp) is assembled into `VERSION_BODY` by `build.rs` and read
4+
//! via `env!("VERSION_BODY")`.
5+
6+
use anyhow::{Result, bail};
7+
8+
// ============================================================================
9+
// Binary target (compile-time)
10+
// ============================================================================
11+
12+
/// Returns the microarchitecture level this binary was compiled for.
13+
pub fn binary_target() -> &'static str {
14+
#[cfg(target_arch = "x86_64")]
15+
{
16+
if cfg!(target_feature = "avx512f") {
17+
"x86-64-v4"
18+
} else if cfg!(target_feature = "avx2") {
19+
"x86-64-v3"
20+
} else {
21+
"x86-64"
22+
}
23+
}
24+
#[cfg(target_arch = "aarch64")]
25+
{
26+
if cfg!(target_feature = "sve") {
27+
"aarch64-neoverse-v1"
28+
} else {
29+
"aarch64"
30+
}
31+
}
32+
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
33+
{
34+
"unknown"
35+
}
36+
}
37+
38+
// ============================================================================
39+
// Runtime CPU feature detection
40+
// ============================================================================
41+
42+
/// Detects CPU features available at runtime, ordered most → least capable.
43+
pub fn detected_features() -> Vec<&'static str> {
44+
let mut features = Vec::new();
45+
46+
#[cfg(target_arch = "x86_64")]
47+
{
48+
if is_x86_feature_detected!("avx512f") {
49+
features.push("AVX-512");
50+
}
51+
if is_x86_feature_detected!("avx2") {
52+
features.push("AVX2");
53+
}
54+
if is_x86_feature_detected!("sse4.2") {
55+
features.push("SSE4.2");
56+
}
57+
if is_x86_feature_detected!("popcnt") {
58+
features.push("POPCNT");
59+
}
60+
}
61+
62+
#[cfg(target_arch = "aarch64")]
63+
{
64+
if std::arch::is_aarch64_feature_detected!("sve2") {
65+
features.push("SVE2");
66+
}
67+
if std::arch::is_aarch64_feature_detected!("sve") {
68+
features.push("SVE");
69+
}
70+
features.push("NEON");
71+
}
72+
73+
features
74+
}
75+
76+
// ============================================================================
77+
// Upgrade hint
78+
// ============================================================================
79+
80+
/// Returns an upgrade suggestion if a faster binary variant is available for
81+
/// the current CPU, `None` if this binary is already optimal.
82+
pub fn upgrade_hint() -> Option<String> {
83+
#[cfg(target_arch = "x86_64")]
84+
{
85+
let has_avx512 = is_x86_feature_detected!("avx512f");
86+
let has_avx2 = is_x86_feature_detected!("avx2");
87+
let target = binary_target();
88+
89+
if target == "x86-64" && has_avx512 {
90+
return Some("Use the x86-64-v4 build for best performance.".to_string());
91+
}
92+
if target == "x86-64" && has_avx2 {
93+
return Some("Use the x86-64-v3 build for better performance.".to_string());
94+
}
95+
if target == "x86-64-v3" && has_avx512 {
96+
return Some("Use the x86-64-v4 build for best performance.".to_string());
97+
}
98+
}
99+
100+
#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
101+
{
102+
let has_sve = std::arch::is_aarch64_feature_detected!("sve");
103+
let target = binary_target();
104+
105+
if target == "aarch64" && has_sve {
106+
return Some("Use the aarch64-neoverse-v1 build for better performance.".to_string());
107+
}
108+
}
109+
110+
// No runtime hint for macOS aarch64: the `apple-m1` variant is a compiler
111+
// tuning target, not a distinct CPU feature set, so runtime detection
112+
// can't tell the two builds apart.
113+
114+
None
115+
}
116+
117+
// ============================================================================
118+
// CPU compatibility guard
119+
// ============================================================================
120+
121+
/// Checks that the CPU supports the features required by this binary.
122+
/// Call at the very start of `main()` — before any SIMD code runs — to give
123+
/// a friendly error instead of a SIGILL crash.
124+
pub fn check_cpu_compat() -> Result<()> {
125+
#[cfg(target_arch = "x86_64")]
126+
match binary_target() {
127+
"x86-64-v4" if !is_x86_feature_detected!("avx512f") => bail!(
128+
"This ruSTAR binary was compiled for x86-64-v4 (AVX-512) but your CPU \
129+
does not support AVX-512.\nPlease use the x86-64-v3 or baseline build instead.\n\
130+
See: https://github.com/Psy-Fer/ruSTAR#installation"
131+
),
132+
"x86-64-v3" if !is_x86_feature_detected!("avx2") => bail!(
133+
"This ruSTAR binary was compiled for x86-64-v3 (AVX2) but your CPU \
134+
does not support AVX2.\nPlease use the baseline build instead.\n\
135+
See: https://github.com/Psy-Fer/ruSTAR#installation"
136+
),
137+
_ => {}
138+
}
139+
140+
#[cfg(target_arch = "aarch64")]
141+
if binary_target() == "aarch64-neoverse-v1" && !std::arch::is_aarch64_feature_detected!("sve") {
142+
bail!(
143+
"This ruSTAR binary was compiled for aarch64-neoverse-v1 (SVE) but your CPU \
144+
does not support SVE.\nPlease use the baseline aarch64 build instead.\n\
145+
See: https://github.com/Psy-Fer/ruSTAR#installation"
146+
);
147+
}
148+
149+
Ok(())
150+
}
151+
152+
/// One-line summary of CPU features detected at runtime. No upgrade hint.
153+
/// Example: `CPU: AVX2 SSE4.2 POPCNT detected`
154+
pub fn cpu_detected_line() -> String {
155+
let features = detected_features();
156+
if features.is_empty() {
157+
"CPU: none detected".to_string()
158+
} else {
159+
format!("CPU: {} detected", features.join(" "))
160+
}
161+
}
162+
163+
#[cfg(test)]
164+
mod tests {
165+
use super::*;
166+
167+
#[test]
168+
fn binary_target_is_not_empty() {
169+
assert!(!binary_target().is_empty());
170+
}
171+
172+
#[test]
173+
fn version_body_contains_expected_fields() {
174+
let body = env!("VERSION_BODY");
175+
assert!(body.contains(env!("GIT_SHORT_HASH")));
176+
assert!(body.contains("built "));
177+
}
178+
179+
#[test]
180+
fn cpu_detected_line_starts_with_cpu() {
181+
assert!(cpu_detected_line().starts_with("CPU:"));
182+
}
183+
184+
#[test]
185+
fn check_cpu_compat_passes_on_current_hardware() {
186+
assert!(check_cpu_compat().is_ok());
187+
}
188+
}

src/lib.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod params;
55

66
pub mod align;
77
pub mod chimeric;
8+
pub mod cpu;
89
pub mod genome;
910
pub mod index;
1011
pub mod io;
@@ -20,12 +21,12 @@ use crate::params::{Parameters, RunMode};
2021
pub fn run(params: &Parameters) -> anyhow::Result<()> {
2122
params.validate()?;
2223

23-
info!(
24-
"ruSTAR v{} ({} built {})",
25-
env!("CARGO_PKG_VERSION"),
26-
env!("GIT_SHORT_HASH"),
27-
env!("BUILD_TIMESTAMP"),
28-
);
24+
info!("ruSTAR {}", env!("CARGO_PKG_VERSION"));
25+
info!("{}", env!("VERSION_BODY"));
26+
info!("{}", cpu::cpu_detected_line());
27+
if let Some(hint) = cpu::upgrade_hint() {
28+
info!("{hint}");
29+
}
2930
info!("runMode: {}", params.run_mode);
3031
info!("runThreadN: {}", params.run_thread_n);
3132

src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use clap::Parser;
22

3+
use ruSTAR::cpu;
34
use ruSTAR::params::Parameters;
45

56
fn main() -> anyhow::Result<()> {
67
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
78

9+
cpu::check_cpu_compat()?;
10+
811
let params = Parameters::parse();
912
ruSTAR::run(&params)
1013
}

src/params.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ impl std::str::FromStr for TwopassMode {
229229
#[command(
230230
name = "ruSTAR",
231231
about = "RNA-seq aligner (Rust reimplementation of STAR)",
232-
version
232+
version,
233+
long_version = concat!(env!("CARGO_PKG_VERSION"), "\n", env!("VERSION_BODY")),
233234
)]
234235
pub struct Parameters {
235236
// ── Run ─────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)