From 1bf9f29da35af76ec759b04487232bae1a043883 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 15:47:45 +0100 Subject: [PATCH 1/8] feat(exec-harness): print command under test before running it --- crates/exec-harness/src/analysis.rs | 1 + crates/exec-harness/src/uri.rs | 14 +++++++++++++- crates/exec-harness/src/walltime/mod.rs | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/exec-harness/src/analysis.rs b/crates/exec-harness/src/analysis.rs index 4469380c..2fcabd1d 100644 --- a/crates/exec-harness/src/analysis.rs +++ b/crates/exec-harness/src/analysis.rs @@ -10,6 +10,7 @@ pub fn perform(commands: Vec) -> Result<()> { for benchmark_cmd in commands { let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); + name_and_uri.print_executing(); let mut cmd = Command::new(&benchmark_cmd.command[0]); cmd.args(&benchmark_cmd.command[1..]); diff --git a/crates/exec-harness/src/uri.rs b/crates/exec-harness/src/uri.rs index 6e599217..77c996a2 100644 --- a/crates/exec-harness/src/uri.rs +++ b/crates/exec-harness/src/uri.rs @@ -3,6 +3,7 @@ use crate::prelude::*; pub struct NameAndUri { pub(crate) name: String, pub(crate) uri: String, + command: Vec, } /// Maximum length for benchmark name to avoid excessively long URIs @@ -22,5 +23,16 @@ pub fn generate_name_and_uri(name: &Option, command: &[String]) -> NameA name.truncate(MAX_NAME_LENGTH); } - NameAndUri { name, uri } + NameAndUri { + name, + uri, + command: command.to_vec(), + } +} + +impl NameAndUri { + pub fn print_executing(&self) { + info!("Executing: {}", self.name); + debug!("Command: {:?}", self.command); + } } diff --git a/crates/exec-harness/src/walltime/mod.rs b/crates/exec-harness/src/walltime/mod.rs index 32310f47..b121063d 100644 --- a/crates/exec-harness/src/walltime/mod.rs +++ b/crates/exec-harness/src/walltime/mod.rs @@ -16,11 +16,13 @@ pub fn perform(commands: Vec) -> Result<()> { for cmd in commands { let name_and_uri = generate_name_and_uri(&cmd.name, &cmd.command); + name_and_uri.print_executing(); let execution_options: ExecutionOptions = cmd.walltime_args.try_into()?; let NameAndUri { name: bench_name, uri: bench_uri, + .. } = name_and_uri; let times_per_round_ns = From 1ee2e06b9505795ca67f5cba38464b0f9a2d75a9 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 15:49:12 +0100 Subject: [PATCH 2/8] feat(exec-harness): add a codspeed_preload library for valgrind instrumentation For now, the library is just generated and its path is passed to the binary through env file. In the next commit, this will change to embed the binary in the final exec-harness executable to make the built artifact self-sufficient. --- .gitignore | 2 + Cargo.lock | 2 + crates/exec-harness/Cargo.toml | 6 +- crates/exec-harness/build.rs | 196 ++++++++++++++++++ .../exec-harness/preload/codspeed_preload.c | 99 +++++++++ crates/exec-harness/src/analysis.rs | 26 +++ crates/exec-harness/src/constants.rs | 15 ++ crates/exec-harness/src/lib.rs | 4 +- 8 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 crates/exec-harness/build.rs create mode 100644 crates/exec-harness/preload/codspeed_preload.c create mode 100644 crates/exec-harness/src/constants.rs diff --git a/.gitignore b/.gitignore index c59e2de8..d847773a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target .DS_Store .codspeed +compile_commands.json +.cache diff --git a/Cargo.lock b/Cargo.lock index 2863e2fa..6f70d1e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -756,6 +756,8 @@ name = "exec-harness" version = "1.0.0" dependencies = [ "anyhow", + "cargo_metadata", + "cc", "clap", "codspeed", "env_logger", diff --git a/crates/exec-harness/Cargo.toml b/crates/exec-harness/Cargo.toml index 27205893..7640b408 100644 --- a/crates/exec-harness/Cargo.toml +++ b/crates/exec-harness/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } -codspeed = "4.1.0" +codspeed = "4.2.0" log = { workspace = true } env_logger = { workspace = true } clap = { workspace = true } @@ -20,6 +20,10 @@ serde = { workspace = true } humantime = "2.1" runner-shared = { path = "../runner-shared" } +[build-dependencies] +cargo_metadata = "0.19" +cc = "1" + [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs new file mode 100644 index 00000000..06954a49 --- /dev/null +++ b/crates/exec-harness/build.rs @@ -0,0 +1,196 @@ +//! Build script for exec-harness +//! +//! This script compiles the `libcodspeed_preload.so` shared library that is used +//! to inject instrumentation into child processes via LD_PRELOAD. +//! +//! The library is built using the `core.c` and headers from the `codspeed` crate's +//! `instrument-hooks` directory. +//! +//! # Environment Variables +//! +//! - `CODSPEED_INSTRUMENT_HOOKS_DIR`: Optional override for the instrument-hooks +//! source directory. If not set, the build script will locate it from the +//! `codspeed` crate in the cargo registry. + +use cargo_metadata::MetadataCommand; +use std::env; +use std::path::PathBuf; + +/// Shared constants for the preload library. +/// These are passed as C defines during compilation and exported as environment +/// variables for the Rust code to use via `env!()`. +struct PreloadConstants { + /// Environment variable name for the benchmark URI. + uri_env: &'static str, + /// Integration name reported to CodSpeed. + integration_name: &'static str, + /// Integration version reported to CodSpeed. + /// This should match the version of the `codspeed` crate dependency. + integration_version: &'static str, +} + +// TODO(COD-1736): Stop impersonating codspeed-rust 🥸 +const PRELOAD_CONSTANTS: PreloadConstants = PreloadConstants { + uri_env: "CODSPEED_BENCH_URI", + integration_name: "codspeed-rust", + integration_version: "4.2.0", +}; + +/// Paths required to build the preload shared library. +struct PreloadBuildPaths { + /// Path to the preload C source file (codspeed_preload.c). + preload_c: PathBuf, + /// Path to the core C source file from instrument-hooks. + core_c: PathBuf, + /// Path to the includes directory from instrument-hooks. + includes_dir: PathBuf, + /// Path where the output shared library will be written. + output_lib: PathBuf, +} + +fn main() { + println!("cargo:rerun-if-changed=preload/codspeed_preload.c"); + println!("cargo:rerun-if-env-changed=CODSPEED_INSTRUMENT_HOOKS_DIR"); + + // Export constants as environment variables for the Rust code + println!( + "cargo:rustc-env=CODSPEED_URI_ENV={}", + PRELOAD_CONSTANTS.uri_env + ); + println!( + "cargo:rustc-env=CODSPEED_INTEGRATION_NAME={}", + PRELOAD_CONSTANTS.integration_name + ); + println!( + "cargo:rustc-env=CODSPEED_INTEGRATION_VERSION={}", + PRELOAD_CONSTANTS.integration_version + ); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + // Try to get the instrument-hooks directory from the environment variable first, + // otherwise locate it from the codspeed crate + let instrument_hooks_dir = match env::var("CODSPEED_INSTRUMENT_HOOKS_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => find_codspeed_instrument_hooks_dir(), + }; + + // Build the preload shared library + let paths = PreloadBuildPaths { + preload_c: manifest_dir.join("preload/codspeed_preload.c"), + core_c: instrument_hooks_dir.join("dist/core.c"), + includes_dir: instrument_hooks_dir.join("includes"), + output_lib: out_dir.join("libcodspeed_preload.so"), + }; + paths.check_all_exist(); + build_shared_library(&paths, &PRELOAD_CONSTANTS); + + // Export the library path for use by the main crate + println!( + "cargo:rustc-env=CODSPEED_PRELOAD_LIB_PATH={}", + paths.output_lib.display() + ); +} + +/// Build the shared library using the cc crate +fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) { + let uri_env_val = format!("\"{}\"", constants.uri_env); + let integration_name_val = format!("\"{}\"", constants.integration_name); + let integration_version_val = format!("\"{}\"", constants.integration_version); + + let mut build = cc::Build::new(); + build + .file(&paths.preload_c) + .file(&paths.core_c) + .include(&paths.includes_dir) + .pic(true) + .opt_level(2) + .cargo_metadata(false) + // Pass constants as C defines + .define("CODSPEED_URI_ENV", uri_env_val.as_str()) + .define("CODSPEED_INTEGRATION_NAME", integration_name_val.as_str()) + .define( + "CODSPEED_INTEGRATION_VERSION", + integration_version_val.as_str(), + ) + // Suppress warnings from generated Zig code + .flag("-Wno-format") + .flag("-Wno-format-security") + .flag("-Wno-unused-but-set-variable") + .flag("-Wno-unused-const-variable") + .flag("-Wno-type-limits") + .flag("-Wno-uninitialized") + .flag("-Wno-overflow") + .flag("-Wno-unused-function"); + + // Compile source files to object files + let objects = build.compile_intermediates(); + + // Link object files into shared library + let compiler = build.get_compiler(); + let mut link_cmd = compiler.to_command(); + link_cmd + .arg("-shared") + .arg("-o") + .arg(&paths.output_lib) + .args(&objects) + .arg("-lpthread"); + + let status = link_cmd.status().expect("Failed to run linker"); + if !status.success() { + panic!("Failed to link libcodspeed_preload.so"); + } +} + +/// Find the instrument-hooks directory from the codspeed crate using cargo_metadata +fn find_codspeed_instrument_hooks_dir() -> PathBuf { + let metadata = MetadataCommand::new() + .exec() + .expect("Failed to run cargo metadata"); + + // Find the codspeed package in the resolved dependencies + let codspeed_pkg = metadata + .packages + .iter() + .find(|p| p.name == "codspeed") + .expect("codspeed crate not found in dependencies"); + + let codspeed_dir = codspeed_pkg + .manifest_path + .parent() + .expect("Failed to get codspeed crate directory"); + + let instrument_hooks_dir = codspeed_dir.join("instrument-hooks"); + + if !instrument_hooks_dir.exists() { + panic!("instrument-hooks directory not found at {instrument_hooks_dir}"); + } + + instrument_hooks_dir.into_std_path_buf() +} + +impl PreloadBuildPaths { + /// Verify that all required source files and directories exist. + /// Panics with a descriptive message if any path is missing. + fn check_all_exist(&self) { + if !self.core_c.exists() { + panic!( + "core.c not found at {}. Make sure the codspeed crate is available.", + self.core_c.display() + ); + } + if !self.includes_dir.exists() { + panic!( + "includes directory not found at {}. Make sure the codspeed crate is available.", + self.includes_dir.display() + ); + } + if !self.preload_c.exists() { + panic!( + "codspeed_preload.c not found at {}", + self.preload_c.display() + ); + } + } +} diff --git a/crates/exec-harness/preload/codspeed_preload.c b/crates/exec-harness/preload/codspeed_preload.c new file mode 100644 index 00000000..0b908e04 --- /dev/null +++ b/crates/exec-harness/preload/codspeed_preload.c @@ -0,0 +1,99 @@ +// LD_PRELOAD library for enabling Valgrind instrumentation in child processes +// +// This library is loaded via LD_PRELOAD into benchmark processes spawned by +// exec-harness. It enables callgrind instrumentation on load and disables it on +// exit, allowing exec-harness to measure arbitrary commands without requiring +// them to link against instrument-hooks. +// +// Environment variables: +// CODSPEED_BENCH_URI - The benchmark URI to report (required) +// CODSPEED_PRELOAD_LOCK - Set by the first process to prevent child processes +// from re-initializing instrumentation + +#include +#include + +#include "core.h" + +#ifndef RUNNING_ON_VALGRIND +// If somehow the core.h did not include the valgrind header, something is +// wrong, but still have a fallback +#warning "RUNNING_ON_VALGRIND not defined, headers may be missing" +#define RUNNING_ON_VALGRIND 0 +#endif + +static const char *LOCK_ENV = "CODSPEED_PRELOAD_LOCK"; + +// These constants are defined by the build script (build.rs) via -D flags +#ifndef CODSPEED_URI_ENV +#error "CODSPEED_URI_ENV must be defined by the build system" +#endif +#ifndef CODSPEED_INTEGRATION_NAME +#error "CODSPEED_INTEGRATION_NAME must be defined by the build system" +#endif +#ifndef CODSPEED_INTEGRATION_VERSION +#error "CODSPEED_INTEGRATION_VERSION must be defined by the build system" +#endif + +static const char *URI_ENV = CODSPEED_URI_ENV; +static const char *INTEGRATION_NAME = CODSPEED_INTEGRATION_NAME; +static const char *INTEGRATION_VERSION = CODSPEED_INTEGRATION_VERSION; + +static InstrumentHooks *g_hooks = NULL; +static const char *g_bench_uri = NULL; + +__attribute__((constructor)) static void codspeed_preload_init(void) { + // Skip initialization if not running under Valgrind yet. + // When using LD_PRELOAD with Valgrind, the constructor runs twice: + // once before Valgrind takes over, and once after. We only want to + // initialize when Valgrind is active. + // + // This is purely empirical, and is not (yet) backed up by documented + // behavior. + if (!RUNNING_ON_VALGRIND) { + return; + } + + // Check if another process already owns the instrumentation + if (getenv(LOCK_ENV)) { + return; + } + + // Set the lock to prevent child processes from initializing + setenv(LOCK_ENV, "1", 1); + + g_bench_uri = getenv(URI_ENV); + if (!g_bench_uri) { + return; + } + + g_hooks = instrument_hooks_init(); + if (!g_hooks) { + return; + } + + instrument_hooks_set_integration(g_hooks, INTEGRATION_NAME, + INTEGRATION_VERSION); + + if (instrument_hooks_start_benchmark_inline(g_hooks) != 0) { + instrument_hooks_deinit(g_hooks); + g_hooks = NULL; + return; + } +} + +__attribute__((destructor)) static void codspeed_preload_fini(void) { + // If the process is not the owner of the lock, this means g_hooks was not + // initialized + if (!g_hooks) { + return; + } + + instrument_hooks_stop_benchmark_inline(g_hooks); + + int32_t pid = getpid(); + instrument_hooks_set_executed_benchmark(g_hooks, pid, g_bench_uri); + + instrument_hooks_deinit(g_hooks); + g_hooks = NULL; +} diff --git a/crates/exec-harness/src/analysis.rs b/crates/exec-harness/src/analysis.rs index 2fcabd1d..e1c7f73c 100644 --- a/crates/exec-harness/src/analysis.rs +++ b/crates/exec-harness/src/analysis.rs @@ -1,6 +1,7 @@ use crate::prelude::*; use crate::BenchmarkCommand; +use crate::constants; use crate::uri; use codspeed::instrument_hooks::InstrumentHooks; use std::process::Command; @@ -28,3 +29,28 @@ pub fn perform(commands: Vec) -> Result<()> { Ok(()) } + +/// Path to the preload library built during compilation. +/// This library is used for LD_PRELOAD-based instrumentation injection. +pub const PRELOAD_LIB_PATH: &str = env!("CODSPEED_PRELOAD_LIB_PATH"); + +pub fn perform_with_valgrind(commands: Vec) -> Result<()> { + for benchmark_cmd in commands { + let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); + name_and_uri.print_executing(); + + let mut cmd = Command::new(&benchmark_cmd.command[0]); + cmd.args(&benchmark_cmd.command[1..]); + // Use LD_PRELOAD to inject instrumentation into the child process + cmd.env("LD_PRELOAD", PRELOAD_LIB_PATH); + cmd.env(constants::URI_ENV, &name_and_uri.uri); + + let status = cmd.status().context("Failed to execute command")?; + + if !status.success() { + bail!("Command exited with non-zero status: {status}"); + } + } + + Ok(()) +} diff --git a/crates/exec-harness/src/constants.rs b/crates/exec-harness/src/constants.rs new file mode 100644 index 00000000..9a47591c --- /dev/null +++ b/crates/exec-harness/src/constants.rs @@ -0,0 +1,15 @@ +//! Shared constants for the exec-harness crate. +//! +//! These constants are defined in the build script (build.rs) and exported as +//! environment variables. The same values are passed to the C preload library +//! as compiler defines, ensuring both Rust and C code use the same source of truth. + +/// Environment variable name for the benchmark URI. +pub const URI_ENV: &str = env!("CODSPEED_URI_ENV"); + +/// Integration name reported to CodSpeed. +pub const INTEGRATION_NAME: &str = env!("CODSPEED_INTEGRATION_NAME"); + +/// Integration version reported to CodSpeed. +/// This should match the version of the `codspeed` crate dependency. +pub const INTEGRATION_VERSION: &str = env!("CODSPEED_INTEGRATION_VERSION"); diff --git a/crates/exec-harness/src/lib.rs b/crates/exec-harness/src/lib.rs index 8fea15ee..b1ed3933 100644 --- a/crates/exec-harness/src/lib.rs +++ b/crates/exec-harness/src/lib.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::io::{self, BufRead}; pub mod analysis; +pub mod constants; pub mod prelude; mod uri; pub mod walltime; @@ -13,6 +14,7 @@ pub mod walltime; pub enum MeasurementMode { Walltime, Memory, + #[value(alias = "instrumentation")] Simulation, } @@ -75,7 +77,7 @@ pub fn execute_benchmarks( analysis::perform(commands)?; } Some(MeasurementMode::Simulation) => { - bail!("Simulation measurement mode is not yet supported by exec-harness"); + analysis::perform_with_valgrind(commands)?; } } From 0b8c2172f507c399db286141b26856b73bde2d95 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 16:11:59 +0100 Subject: [PATCH 3/8] feat(exec-harness): embed the preload library in the binary --- crates/exec-harness/Cargo.toml | 4 +-- crates/exec-harness/build.rs | 16 ++++----- crates/exec-harness/src/analysis.rs | 56 +++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/crates/exec-harness/Cargo.toml b/crates/exec-harness/Cargo.toml index 7640b408..e5a9bfbb 100644 --- a/crates/exec-harness/Cargo.toml +++ b/crates/exec-harness/Cargo.toml @@ -19,13 +19,11 @@ serde_json = { workspace = true } serde = { workspace = true } humantime = "2.1" runner-shared = { path = "../runner-shared" } +tempfile = { workspace = true } [build-dependencies] cargo_metadata = "0.19" cc = "1" -[dev-dependencies] -tempfile = { workspace = true } - [package.metadata.dist] targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs index 06954a49..f10caadc 100644 --- a/crates/exec-harness/build.rs +++ b/crates/exec-harness/build.rs @@ -36,6 +36,9 @@ const PRELOAD_CONSTANTS: PreloadConstants = PreloadConstants { integration_version: "4.2.0", }; +/// Filename for the preload shared library. +const PRELOAD_LIB_FILENAME: &str = "libcodspeed_preload.so"; + /// Paths required to build the preload shared library. struct PreloadBuildPaths { /// Path to the preload C source file (codspeed_preload.c). @@ -65,6 +68,7 @@ fn main() { "cargo:rustc-env=CODSPEED_INTEGRATION_VERSION={}", PRELOAD_CONSTANTS.integration_version ); + println!("cargo:rustc-env=CODSPEED_PRELOAD_LIB_FILENAME={PRELOAD_LIB_FILENAME}"); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -81,16 +85,10 @@ fn main() { preload_c: manifest_dir.join("preload/codspeed_preload.c"), core_c: instrument_hooks_dir.join("dist/core.c"), includes_dir: instrument_hooks_dir.join("includes"), - output_lib: out_dir.join("libcodspeed_preload.so"), + output_lib: out_dir.join(PRELOAD_LIB_FILENAME), }; - paths.check_all_exist(); + paths.check_sources_exist(); build_shared_library(&paths, &PRELOAD_CONSTANTS); - - // Export the library path for use by the main crate - println!( - "cargo:rustc-env=CODSPEED_PRELOAD_LIB_PATH={}", - paths.output_lib.display() - ); } /// Build the shared library using the cc crate @@ -173,7 +171,7 @@ fn find_codspeed_instrument_hooks_dir() -> PathBuf { impl PreloadBuildPaths { /// Verify that all required source files and directories exist. /// Panics with a descriptive message if any path is missing. - fn check_all_exist(&self) { + fn check_sources_exist(&self) { if !self.core_c.exists() { panic!( "core.c not found at {}. Make sure the codspeed crate is available.", diff --git a/crates/exec-harness/src/analysis.rs b/crates/exec-harness/src/analysis.rs index e1c7f73c..f64aab29 100644 --- a/crates/exec-harness/src/analysis.rs +++ b/crates/exec-harness/src/analysis.rs @@ -4,7 +4,10 @@ use crate::BenchmarkCommand; use crate::constants; use crate::uri; use codspeed::instrument_hooks::InstrumentHooks; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; use std::process::Command; +use std::sync::OnceLock; pub fn perform(commands: Vec) -> Result<()> { let hooks = InstrumentHooks::instance(); @@ -30,11 +33,58 @@ pub fn perform(commands: Vec) -> Result<()> { Ok(()) } -/// Path to the preload library built during compilation. +/// Filename for the preload shared library. +const PRELOAD_LIB_FILENAME: &str = env!("CODSPEED_PRELOAD_LIB_FILENAME"); + +/// The preload library binary embedded at compile time. /// This library is used for LD_PRELOAD-based instrumentation injection. -pub const PRELOAD_LIB_PATH: &str = env!("CODSPEED_PRELOAD_LIB_PATH"); +const PRELOAD_LIB_BYTES: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/", + env!("CODSPEED_PRELOAD_LIB_FILENAME") +)); + +/// Lazily initialized temp file containing the extracted preload library. +/// Kept in a static to prevent cleanup until process exit. +static PRELOAD_LIB_FILE: OnceLock = OnceLock::new(); + +/// Extracts the preload library to a temp file. +fn extract_preload_lib() -> Result { + let mut file = tempfile::Builder::new() + .prefix(PRELOAD_LIB_FILENAME) + .tempfile() + .context("Failed to create temp file for preload library")?; + + file.write_all(PRELOAD_LIB_BYTES) + .context("Failed to write preload library to temp file")?; + + // Make the library executable + let mut permissions = file + .as_file() + .metadata() + .context("Failed to get temp file metadata")? + .permissions(); + permissions.set_mode(0o755); + file.as_file() + .set_permissions(permissions) + .context("Failed to set temp file permissions")?; + + Ok(file) +} + +/// Returns the path to the preload library, extracting it to a temp file if needed. +fn get_preload_lib_path() -> Result<&'static std::path::Path> { + if let Some(file) = PRELOAD_LIB_FILE.get() { + return Ok(file.path()); + } + + let file = extract_preload_lib()?; + Ok(PRELOAD_LIB_FILE.get_or_init(|| file).path()) +} pub fn perform_with_valgrind(commands: Vec) -> Result<()> { + let preload_lib_path = get_preload_lib_path()?; + for benchmark_cmd in commands { let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); name_and_uri.print_executing(); @@ -42,7 +92,7 @@ pub fn perform_with_valgrind(commands: Vec) -> Result<()> { let mut cmd = Command::new(&benchmark_cmd.command[0]); cmd.args(&benchmark_cmd.command[1..]); // Use LD_PRELOAD to inject instrumentation into the child process - cmd.env("LD_PRELOAD", PRELOAD_LIB_PATH); + cmd.env("LD_PRELOAD", preload_lib_path); cmd.env(constants::URI_ENV, &name_and_uri.uri); let status = cmd.status().context("Failed to execute command")?; From 4bee3df0285dc96f2d711e5030c789fef620ed41 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 16:40:30 +0100 Subject: [PATCH 4/8] refactor: move the preload_file management to its own module Also switch to tempfile --- crates/exec-harness/src/analysis.rs | 106 ------------------ crates/exec-harness/src/analysis/mod.rs | 61 ++++++++++ .../src/analysis/preload_lib_file.rs | 46 ++++++++ 3 files changed, 107 insertions(+), 106 deletions(-) delete mode 100644 crates/exec-harness/src/analysis.rs create mode 100644 crates/exec-harness/src/analysis/mod.rs create mode 100644 crates/exec-harness/src/analysis/preload_lib_file.rs diff --git a/crates/exec-harness/src/analysis.rs b/crates/exec-harness/src/analysis.rs deleted file mode 100644 index f64aab29..00000000 --- a/crates/exec-harness/src/analysis.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; - -use crate::BenchmarkCommand; -use crate::constants; -use crate::uri; -use codspeed::instrument_hooks::InstrumentHooks; -use std::io::Write; -use std::os::unix::fs::PermissionsExt; -use std::process::Command; -use std::sync::OnceLock; - -pub fn perform(commands: Vec) -> Result<()> { - let hooks = InstrumentHooks::instance(); - - for benchmark_cmd in commands { - let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); - name_and_uri.print_executing(); - - let mut cmd = Command::new(&benchmark_cmd.command[0]); - cmd.args(&benchmark_cmd.command[1..]); - hooks.start_benchmark().unwrap(); - let status = cmd.status(); - hooks.stop_benchmark().unwrap(); - let status = status.context("Failed to execute command")?; - - if !status.success() { - bail!("Command exited with non-zero status: {status}"); - } - - hooks.set_executed_benchmark(&name_and_uri.uri).unwrap(); - } - - Ok(()) -} - -/// Filename for the preload shared library. -const PRELOAD_LIB_FILENAME: &str = env!("CODSPEED_PRELOAD_LIB_FILENAME"); - -/// The preload library binary embedded at compile time. -/// This library is used for LD_PRELOAD-based instrumentation injection. -const PRELOAD_LIB_BYTES: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), - "/", - env!("CODSPEED_PRELOAD_LIB_FILENAME") -)); - -/// Lazily initialized temp file containing the extracted preload library. -/// Kept in a static to prevent cleanup until process exit. -static PRELOAD_LIB_FILE: OnceLock = OnceLock::new(); - -/// Extracts the preload library to a temp file. -fn extract_preload_lib() -> Result { - let mut file = tempfile::Builder::new() - .prefix(PRELOAD_LIB_FILENAME) - .tempfile() - .context("Failed to create temp file for preload library")?; - - file.write_all(PRELOAD_LIB_BYTES) - .context("Failed to write preload library to temp file")?; - - // Make the library executable - let mut permissions = file - .as_file() - .metadata() - .context("Failed to get temp file metadata")? - .permissions(); - permissions.set_mode(0o755); - file.as_file() - .set_permissions(permissions) - .context("Failed to set temp file permissions")?; - - Ok(file) -} - -/// Returns the path to the preload library, extracting it to a temp file if needed. -fn get_preload_lib_path() -> Result<&'static std::path::Path> { - if let Some(file) = PRELOAD_LIB_FILE.get() { - return Ok(file.path()); - } - - let file = extract_preload_lib()?; - Ok(PRELOAD_LIB_FILE.get_or_init(|| file).path()) -} - -pub fn perform_with_valgrind(commands: Vec) -> Result<()> { - let preload_lib_path = get_preload_lib_path()?; - - for benchmark_cmd in commands { - let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); - name_and_uri.print_executing(); - - let mut cmd = Command::new(&benchmark_cmd.command[0]); - cmd.args(&benchmark_cmd.command[1..]); - // Use LD_PRELOAD to inject instrumentation into the child process - cmd.env("LD_PRELOAD", preload_lib_path); - cmd.env(constants::URI_ENV, &name_and_uri.uri); - - let status = cmd.status().context("Failed to execute command")?; - - if !status.success() { - bail!("Command exited with non-zero status: {status}"); - } - } - - Ok(()) -} diff --git a/crates/exec-harness/src/analysis/mod.rs b/crates/exec-harness/src/analysis/mod.rs new file mode 100644 index 00000000..6d68eb3b --- /dev/null +++ b/crates/exec-harness/src/analysis/mod.rs @@ -0,0 +1,61 @@ +use crate::prelude::*; + +use crate::BenchmarkCommand; +use crate::constants; +use crate::uri; +use codspeed::instrument_hooks::InstrumentHooks; +use std::process::Command; + +mod preload_lib_file; + +pub fn perform(commands: Vec) -> Result<()> { + let hooks = InstrumentHooks::instance(); + + for benchmark_cmd in commands { + let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); + name_and_uri.print_executing(); + + let mut cmd = Command::new(&benchmark_cmd.command[0]); + cmd.args(&benchmark_cmd.command[1..]); + hooks.start_benchmark().unwrap(); + let status = cmd.status(); + hooks.stop_benchmark().unwrap(); + let status = status.context("Failed to execute command")?; + + if !status.success() { + bail!("Command exited with non-zero status: {status}"); + } + + hooks.set_executed_benchmark(&name_and_uri.uri).unwrap(); + } + + Ok(()) +} + +/// Executes the given benchmark commands using a preload based trick to handle valgrind control. +/// +/// This function is only supported on Unix-like platforms, as it relies on the +/// `LD_PRELOAD` environment variable and Unix file permissions for shared libraries. +/// It will not work on non-Unix platforms. +pub fn perform_with_valgrind(commands: Vec) -> Result<()> { + let preload_lib_path = preload_lib_file::get_preload_lib_path()?; + + for benchmark_cmd in commands { + let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); + name_and_uri.print_executing(); + + let mut cmd = Command::new(&benchmark_cmd.command[0]); + cmd.args(&benchmark_cmd.command[1..]); + // Use LD_PRELOAD to inject instrumentation into the child process + cmd.env("LD_PRELOAD", preload_lib_path); + cmd.env(constants::URI_ENV, &name_and_uri.uri); + + let status = cmd.status().context("Failed to execute command")?; + + if !status.success() { + bail!("Command exited with non-zero status: {status}"); + } + } + + Ok(()) +} diff --git a/crates/exec-harness/src/analysis/preload_lib_file.rs b/crates/exec-harness/src/analysis/preload_lib_file.rs new file mode 100644 index 00000000..2d53804c --- /dev/null +++ b/crates/exec-harness/src/analysis/preload_lib_file.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; + +use std::io::Write; +use std::sync::OnceLock; + +/// Filename for the preload shared library. +const PRELOAD_LIB_FILENAME: &str = env!("CODSPEED_PRELOAD_LIB_FILENAME"); + +/// The preload library binary embedded at compile time. +const PRELOAD_LIB_BYTES: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/", + env!("CODSPEED_PRELOAD_LIB_FILENAME") +)); + +/// Lazily initialized temp file containing the extracted preload library. +/// Kept in a static to prevent cleanup until process exit. +static PRELOAD_LIB_FILE: OnceLock = OnceLock::new(); + +/// Extracts the preload library to a temp file. +fn extract_preload_lib() -> Result { + let mut file = tempfile::Builder::new() + .suffix(PRELOAD_LIB_FILENAME) + .tempfile() + .context("Failed to create temp file for preload library")?; + + file.write_all(PRELOAD_LIB_BYTES) + .context("Failed to write preload library to temp file")?; + + debug!( + "Extracted preload library to temp file: {}", + file.path().display() + ); + + Ok(file) +} + +/// Returns the path to the preload library, extracting it to a temp file if needed. +pub(super) fn get_preload_lib_path() -> Result<&'static std::path::Path> { + if let Some(file) = PRELOAD_LIB_FILE.get() { + return Ok(file.path()); + } + + let file = extract_preload_lib()?; + Ok(PRELOAD_LIB_FILE.get_or_init(|| file).path()) +} From dd2113dffa161f3c2a545dd69e42c4261f226bbc Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 18:35:58 +0100 Subject: [PATCH 5/8] feat(exec-harness): bail when used with a statically linked binary --- Cargo.lock | 7 +- Cargo.toml | 4 + crates/exec-harness/Cargo.toml | 1 + .../src/analysis/ld_preload_check.rs | 120 ++++++++++++++++++ crates/exec-harness/src/analysis/mod.rs | 6 +- crates/memtrack/Cargo.toml | 2 +- 6 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 crates/exec-harness/src/analysis/ld_preload_check.rs diff --git a/Cargo.lock b/Cargo.lock index 6f70d1e0..a686df35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,7 +494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -763,6 +763,7 @@ dependencies = [ "env_logger", "humantime", "log", + "object 0.36.7", "runner-shared", "serde", "serde_json", @@ -2215,7 +2216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.111", @@ -2596,7 +2597,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 191e1d37..c132c79e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,10 @@ ipc-channel = "0.18" itertools = "0.14.0" env_logger = "0.11.8" tempfile = "3.10.0" +object = { version = "0.36", default-features = false, features = [ + "read_core", + "elf", +] } [workspace.metadata.release] sign-tag = true diff --git a/crates/exec-harness/Cargo.toml b/crates/exec-harness/Cargo.toml index e5a9bfbb..0c6168e5 100644 --- a/crates/exec-harness/Cargo.toml +++ b/crates/exec-harness/Cargo.toml @@ -20,6 +20,7 @@ serde = { workspace = true } humantime = "2.1" runner-shared = { path = "../runner-shared" } tempfile = { workspace = true } +object = { workspace = true } [build-dependencies] cargo_metadata = "0.19" diff --git a/crates/exec-harness/src/analysis/ld_preload_check.rs b/crates/exec-harness/src/analysis/ld_preload_check.rs new file mode 100644 index 00000000..702f18d2 --- /dev/null +++ b/crates/exec-harness/src/analysis/ld_preload_check.rs @@ -0,0 +1,120 @@ +use crate::prelude::*; +use std::fs; +use std::path::Path; + +/// Checks if the given executable will honor LD_PRELOAD. +/// +/// Returns `Ok(())` if LD_PRELOAD will work, or an error with a descriptive message if not. +/// +/// LD_PRELOAD works for: +/// - Dynamically linked ELF binaries +/// - Scripts (the interpreter is typically dynamically linked) +/// +/// LD_PRELOAD does NOT work for: +/// - Statically linked ELF binaries (no dynamic linker involved) +pub fn check_ld_preload_compatible(executable: &str) -> Result<()> { + let path = resolve_executable(executable)?; + let data = fs::read(&path) + .with_context(|| format!("Failed to read executable: {}", path.display()))?; + + // Check ELF magic bytes + if data.len() >= 4 && &data[0..4] == b"\x7FELF" { + check_elf_is_dynamic(&data, &path) + } else { + // Not an ELF file - likely a script with a shebang. + // Scripts use an interpreter which is typically dynamically linked. + Ok(()) + } +} + +/// Resolve executable name to its full path using PATH lookup. +fn resolve_executable(executable: &str) -> Result { + let path = Path::new(executable); + + // If it's already an absolute or relative path, use it directly + if path.is_absolute() || executable.contains('/') { + return Ok(path.to_path_buf()); + } + + // Search in PATH + if let Ok(path_env) = std::env::var("PATH") { + for dir in path_env.split(':') { + let candidate = Path::new(dir).join(executable); + if candidate.is_file() { + return Ok(candidate); + } + } + } + + bail!("Executable not found in PATH: {executable}") +} + +/// Check if an ELF binary is dynamically linked. +fn check_elf_is_dynamic(data: &[u8], path: &Path) -> Result<()> { + use object::Endianness; + use object::read::elf::ElfFile; + + // Try parsing as 64-bit ELF first, then 32-bit + if let Ok(elf) = ElfFile::>::parse(data) { + return check_elf_has_interp(elf, path); + } + + if let Ok(elf) = ElfFile::>::parse(data) { + return check_elf_has_interp(elf, path); + } + + bail!("Failed to parse ELF file: {}", path.display()) +} + +/// Check if an ELF file has a PT_INTERP or PT_DYNAMIC segment, indicating dynamic linking. +fn check_elf_has_interp<'data, Elf>( + elf: object::read::elf::ElfFile<'data, Elf>, + path: &Path, +) -> Result<()> +where + Elf: object::read::elf::FileHeader, +{ + use object::read::elf::ProgramHeader; + + let endian = elf.endian(); + + for segment in elf.elf_program_headers() { + let p_type = segment.p_type(endian); + // Either PT_INTERP or PT_DYNAMIC indicates a dynamically linked binary + if p_type == object::elf::PT_INTERP || p_type == object::elf::PT_DYNAMIC { + return Ok(()); + } + } + + // No PT_INTERP found - this is a statically linked binary + bail!( + "The codspeed CLI in CPU Simulation mode does not support statically linked binaries.\n\n\ + Executable '{}' is statically linked.\n\n\ + Please either:\n\ + - Use a dynamically linked executable, or\n\ + - Use a different measurement mode, or\n\ + - Use one of the CodSpeed framework benchmark integrations", + path.display() + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dynamic_binary() { + // /bin/sh or similar should be dynamically linked on most systems + let result = check_ld_preload_compatible("sh"); + assert!( + result.is_ok(), + "sh should be dynamically linked: {result:?}" + ); + } + + #[test] + fn test_nonexistent_binary() { + let result = check_ld_preload_compatible("nonexistent_binary_12345"); + assert!(result.is_err()); + } +} diff --git a/crates/exec-harness/src/analysis/mod.rs b/crates/exec-harness/src/analysis/mod.rs index 6d68eb3b..49442d1c 100644 --- a/crates/exec-harness/src/analysis/mod.rs +++ b/crates/exec-harness/src/analysis/mod.rs @@ -6,6 +6,7 @@ use crate::uri; use codspeed::instrument_hooks::InstrumentHooks; use std::process::Command; +mod ld_preload_check; mod preload_lib_file; pub fn perform(commands: Vec) -> Result<()> { @@ -36,11 +37,14 @@ pub fn perform(commands: Vec) -> Result<()> { /// /// This function is only supported on Unix-like platforms, as it relies on the /// `LD_PRELOAD` environment variable and Unix file permissions for shared libraries. -/// It will not work on non-Unix platforms. +/// It will not work on non-Unix platforms or with statically linked binaries. pub fn perform_with_valgrind(commands: Vec) -> Result<()> { let preload_lib_path = preload_lib_file::get_preload_lib_path()?; for benchmark_cmd in commands { + // Check if the executable will honor LD_PRELOAD before running + ld_preload_check::check_ld_preload_compatible(&benchmark_cmd.command[0])?; + let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); name_and_uri.print_executing(); diff --git a/crates/memtrack/Cargo.toml b/crates/memtrack/Cargo.toml index 178fc61a..28b80f4e 100644 --- a/crates/memtrack/Cargo.toml +++ b/crates/memtrack/Cargo.toml @@ -33,7 +33,7 @@ itertools = { workspace = true } paste = "1.0" libbpf-rs = { version = "0.25.0", features = ["vendored"], optional = true } glob = "0.3.3" -object = { version = "0.36", default-features = false, features = ["read_core", "elf"] } +object = { workspace = true } [build-dependencies] libbpf-cargo = { version = "0.25.0", optional = true } From e29c367044b00c2623852588b2b280d630ebc700 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 22 Jan 2026 18:56:40 +0100 Subject: [PATCH 6/8] feat: clean-up the build script --- crates/exec-harness/build.rs | 71 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs index f10caadc..2c05f52e 100644 --- a/crates/exec-harness/build.rs +++ b/crates/exec-harness/build.rs @@ -27,50 +27,34 @@ struct PreloadConstants { /// Integration version reported to CodSpeed. /// This should match the version of the `codspeed` crate dependency. integration_version: &'static str, -} - -// TODO(COD-1736): Stop impersonating codspeed-rust 🥸 -const PRELOAD_CONSTANTS: PreloadConstants = PreloadConstants { - uri_env: "CODSPEED_BENCH_URI", - integration_name: "codspeed-rust", - integration_version: "4.2.0", -}; - -/// Filename for the preload shared library. -const PRELOAD_LIB_FILENAME: &str = "libcodspeed_preload.so"; - -/// Paths required to build the preload shared library. -struct PreloadBuildPaths { - /// Path to the preload C source file (codspeed_preload.c). - preload_c: PathBuf, - /// Path to the core C source file from instrument-hooks. - core_c: PathBuf, - /// Path to the includes directory from instrument-hooks. - includes_dir: PathBuf, - /// Path where the output shared library will be written. - output_lib: PathBuf, + /// Filename for the preload shared library. + preload_lib_filename: &'static str, } fn main() { println!("cargo:rerun-if-changed=preload/codspeed_preload.c"); println!("cargo:rerun-if-env-changed=CODSPEED_INSTRUMENT_HOOKS_DIR"); + let preload_constants: PreloadConstants = PreloadConstants::default(); + // Export constants as environment variables for the Rust code println!( "cargo:rustc-env=CODSPEED_URI_ENV={}", - PRELOAD_CONSTANTS.uri_env + preload_constants.uri_env ); println!( "cargo:rustc-env=CODSPEED_INTEGRATION_NAME={}", - PRELOAD_CONSTANTS.integration_name + preload_constants.integration_name ); println!( "cargo:rustc-env=CODSPEED_INTEGRATION_VERSION={}", - PRELOAD_CONSTANTS.integration_version + preload_constants.integration_version + ); + println!( + "cargo:rustc-env=CODSPEED_PRELOAD_LIB_FILENAME={}", + preload_constants.preload_lib_filename ); - println!("cargo:rustc-env=CODSPEED_PRELOAD_LIB_FILENAME={PRELOAD_LIB_FILENAME}"); - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); // Try to get the instrument-hooks directory from the environment variable first, @@ -85,10 +69,9 @@ fn main() { preload_c: manifest_dir.join("preload/codspeed_preload.c"), core_c: instrument_hooks_dir.join("dist/core.c"), includes_dir: instrument_hooks_dir.join("includes"), - output_lib: out_dir.join(PRELOAD_LIB_FILENAME), }; paths.check_sources_exist(); - build_shared_library(&paths, &PRELOAD_CONSTANTS); + build_shared_library(&paths, &preload_constants); } /// Build the shared library using the cc crate @@ -96,6 +79,8 @@ fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) let uri_env_val = format!("\"{}\"", constants.uri_env); let integration_name_val = format!("\"{}\"", constants.integration_name); let integration_version_val = format!("\"{}\"", constants.integration_version); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let out_file = out_dir.join(constants.preload_lib_filename); let mut build = cc::Build::new(); build @@ -103,7 +88,9 @@ fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) .file(&paths.core_c) .include(&paths.includes_dir) .pic(true) - .opt_level(2) + .opt_level(3) + // There's no need to output cargo metadata as we are just building a shared library + // that will be copied to disk and loaded through LD_PRELOAD at runtime .cargo_metadata(false) // Pass constants as C defines .define("CODSPEED_URI_ENV", uri_env_val.as_str()) @@ -131,7 +118,7 @@ fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) link_cmd .arg("-shared") .arg("-o") - .arg(&paths.output_lib) + .arg(&out_file) .args(&objects) .arg("-lpthread"); @@ -168,6 +155,28 @@ fn find_codspeed_instrument_hooks_dir() -> PathBuf { instrument_hooks_dir.into_std_path_buf() } +impl Default for PreloadConstants { + // TODO(COD-1736): Stop impersonating codspeed-rust 🥸 + fn default() -> Self { + Self { + uri_env: "CODSPEED_BENCH_URI", + integration_name: "codspeed-rust", + integration_version: "4.2.0", + preload_lib_filename: "libcodspeed_preload.so", + } + } +} + +/// Paths required to build the preload shared library. +struct PreloadBuildPaths { + /// Path to the preload C source file (codspeed_preload.c). + preload_c: PathBuf, + /// Path to the core C source file from instrument-hooks. + core_c: PathBuf, + /// Path to the includes directory from instrument-hooks. + includes_dir: PathBuf, +} + impl PreloadBuildPaths { /// Verify that all required source files and directories exist. /// Panics with a descriptive message if any path is missing. From bb33f05c8fafd4b98ac586352f89974719264915 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 23 Jan 2026 05:56:44 +0100 Subject: [PATCH 7/8] chore: change arm runner --- dist-workspace.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist-workspace.toml b/dist-workspace.toml index 2a6b736c..d60dffaf 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -34,4 +34,4 @@ local-artifacts-jobs = ["./create-draft-release"] allow-dirty = ["ci"] [dist.github-custom-runners] -aarch64-unknown-linux-musl = "buildjet-2vcpu-ubuntu-2204-arm" +aarch64-unknown-linux-musl = "codspeedhq-arm64-ubuntu-24.04" From a90b8ffcc22d6d31a9ea41fc3f68ed110dff95eb Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 23 Jan 2026 08:16:11 +0100 Subject: [PATCH 8/8] chore: build exec-harness in gnu for release --- crates/exec-harness/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/exec-harness/Cargo.toml b/crates/exec-harness/Cargo.toml index 0c6168e5..cd8a034b 100644 --- a/crates/exec-harness/Cargo.toml +++ b/crates/exec-harness/Cargo.toml @@ -27,4 +27,4 @@ cargo_metadata = "0.19" cc = "1" [package.metadata.dist] -targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +targets = ["aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]