diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index 10e12edc8..2c4491048 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -12,6 +12,10 @@ categories.workspace = true [lib] crate-type = ["cdylib"] +[[test]] +name = "x86_64_v2_build_flags" +path = "build_support/x86_64_v2.rs" + [dependencies] async-trait.workspace = true arrow-ipc.workspace = true diff --git a/nodejs/build.rs b/nodejs/build.rs index b846529b9..4603899b0 100644 --- a/nodejs/build.rs +++ b/nodejs/build.rs @@ -5,22 +5,15 @@ extern crate napi_build; use std::env; +#[path = "build_support/x86_64_v2.rs"] +mod x86_64_v2; + const ENFORCE_BASELINE: &str = "LANCEDB_NODE_ENFORCE_X86_64_V2"; -const X86_64_V2_FEATURES: [&str; 9] = [ - "cmpxchg16b", - "fxsr", - "popcnt", - "sse", - "sse2", - "sse3", - "sse4.1", - "sse4.2", - "ssse3", -]; fn main() { napi_build::setup(); println!("cargo:rerun-if-env-changed={ENFORCE_BASELINE}"); + println!("cargo:rerun-if-env-changed=CARGO_ENCODED_RUSTFLAGS"); let is_linux_x64 = env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("x86_64") && env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux"); @@ -31,15 +24,8 @@ fn main() { return; } - let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); - let features_above_v2 = target_features - .split(',') - .filter(|feature| !feature.is_empty() && !X86_64_V2_FEATURES.contains(feature)) - .collect::>(); - - assert!( - features_above_v2.is_empty(), - "Linux x64 Node addons must use the x86-64-v2 baseline; features above v2: {}", - features_above_v2.join(", ") - ); + let encoded_rustflags = env::var("CARGO_ENCODED_RUSTFLAGS").unwrap_or_default(); + x86_64_v2::validate_encoded_rustflags(&encoded_rustflags).unwrap_or_else(|error| { + panic!("Linux x64 Node addons must use the x86-64-v2 baseline; {error}") + }); } diff --git a/nodejs/build_support/x86_64_v2.rs b/nodejs/build_support/x86_64_v2.rs new file mode 100644 index 000000000..61d455a23 --- /dev/null +++ b/nodejs/build_support/x86_64_v2.rs @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +use std::collections::{BTreeMap, BTreeSet}; + +const BASELINE_CPU: &str = "x86-64-v2"; +const BASELINE_FEATURES: [&str; 9] = [ + "cmpxchg16b", + "fxsr", + "popcnt", + "sse", + "sse2", + "sse3", + "sse4.1", + "sse4.2", + "ssse3", +]; + +pub(crate) fn validate_encoded_rustflags(encoded: &str) -> Result<(), String> { + let mut target_cpu = None; + let mut feature_states = BTreeMap::new(); + let mut required_disables = BTreeSet::new(); + let mut unsupported_features = BTreeSet::new(); + + for option in codegen_options(encoded)? { + let Some((name, value)) = option.split_once('=') else { + continue; + }; + + match name { + "target-cpu" => target_cpu = Some(value), + "target-feature" => { + for toggle in value.split(',').filter(|toggle| !toggle.is_empty()) { + let (enabled, feature) = match toggle.as_bytes()[0] { + b'+' => (true, &toggle[1..]), + b'-' => (false, &toggle[1..]), + _ => return Err(format!("invalid target feature flag: {toggle}")), + }; + + feature_states.insert(feature, enabled); + if enabled && !BASELINE_FEATURES.contains(&feature) { + match feature { + // These are inherited from the workspace configuration and + // explicitly canceled by the Node configuration. Account for + // their implied AVX prerequisite as well as the named feature. + "avx" => { + required_disables.insert("avx"); + } + "avx2" => { + required_disables.extend(["avx", "avx2"]); + } + "f16c" => { + required_disables.extend(["avx", "f16c"]); + } + "fma" => { + required_disables.extend(["avx", "fma"]); + } + _ => { + unsupported_features.insert(feature); + } + } + } + } + } + // LLVM arguments can independently alter the target feature set and + // cannot be proven safe by inspecting rustc's target options. + "llvm-args" => return Err("LLVM arguments can override the CPU baseline".to_owned()), + _ => {} + } + } + + if target_cpu != Some(BASELINE_CPU) { + return Err(format!( + "effective target CPU is {}, expected {BASELINE_CPU}", + target_cpu.unwrap_or("unset") + )); + } + + if !unsupported_features.is_empty() { + return Err(format!( + "features above v2: {}", + unsupported_features + .into_iter() + .collect::>() + .join(", ") + )); + } + + let not_disabled = required_disables + .into_iter() + .filter(|feature| feature_states.get(feature) != Some(&false)) + .collect::>(); + if !not_disabled.is_empty() { + return Err(format!( + "inherited features not fully disabled: {}", + not_disabled.join(", ") + )); + } + + Ok(()) +} + +fn codegen_options(encoded: &str) -> Result, String> { + let arguments = encoded.split('\u{1f}').collect::>(); + let mut options = Vec::new(); + let mut index = 0; + + while index < arguments.len() { + let argument = arguments[index]; + if argument == "-C" || argument == "--codegen" { + index += 1; + let option = arguments + .get(index) + .copied() + .ok_or_else(|| format!("missing value after {argument}"))?; + options.push(option.trim_start_matches('=')); + } else if let Some(option) = argument.strip_prefix("-C") { + if !option.is_empty() { + options.push(option.trim_start_matches('=')); + } + } else if let Some(option) = argument.strip_prefix("--codegen=") { + options.push(option); + } + index += 1; + } + + Ok(options) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn encoded(arguments: &[&str]) -> String { + arguments.join("\u{1f}") + } + + #[test] + fn accepts_merged_workspace_and_node_flags() { + let flags = encoded(&[ + "-C", + "target-cpu=haswell", + "-C", + "target-feature=+avx2,+fma,+f16c", + "-C", + "target-cpu=x86-64-v2", + "-C", + "target-feature=-avx,-avx2,-fma,-f16c", + ]); + + assert_eq!(validate_encoded_rustflags(&flags), Ok(())); + } + + #[test] + fn accepts_non_merging_v2_boundary() { + let flags = encoded(&["-Ctarget-cpu=x86-64-v2"]); + + assert_eq!(validate_encoded_rustflags(&flags), Ok(())); + } + + #[test] + fn accepts_musl_dynamic_crt_configuration() { + let flags = encoded(&[ + "-C", + "target-cpu=haswell", + "-C", + "target-feature=-crt-static,+avx2,+fma,+f16c", + "-C", + "target-cpu=x86-64-v2", + "-C", + "target-feature=-crt-static,-avx,-avx2,-fma,-f16c", + ]); + + assert_eq!(validate_encoded_rustflags(&flags), Ok(())); + } + + #[test] + fn rejects_feature_omitted_from_target_cfg() { + let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Ctarget-feature=+apxf"]); + + assert_eq!( + validate_encoded_rustflags(&flags), + Err("features above v2: apxf".to_owned()) + ); + } + + #[test] + fn rejects_unexpected_above_baseline_feature() { + let flags = encoded(&[ + "--codegen=target-cpu=x86-64-v2", + "--codegen", + "target-feature=+bmi2", + ]); + + assert_eq!( + validate_encoded_rustflags(&flags), + Err("features above v2: bmi2".to_owned()) + ); + } + + #[test] + fn rejects_incompletely_disabled_feature_implications() { + let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Ctarget-feature=+avx2,-avx2"]); + + assert_eq!( + validate_encoded_rustflags(&flags), + Err("inherited features not fully disabled: avx".to_owned()) + ); + } + + #[test] + fn rejects_llvm_feature_overrides() { + let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Cllvm-args=-mattr=+apxf"]); + + assert_eq!( + validate_encoded_rustflags(&flags), + Err("LLVM arguments can override the CPU baseline".to_owned()) + ); + } +}