diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27f62812..847e371e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,21 @@ jobs: - name: Clean up run: sudo chown -R $USER:$USER . ~/.cargo + windows-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + - name: "Install rust-toolchain.toml" + run: rustup toolchain install + - name: Add Windows GNU target + run: rustup target add x86_64-pc-windows-gnu + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + - name: Check codspeed-runner builds on Windows + # Instrument hooks fails to build with msvc target, so we use the gnu target for this check for now + run: cargo check -p codspeed-runner --target x86_64-pc-windows-gnu + benchmarks: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 42aa1a05..4d7e5c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,6 +691,7 @@ dependencies = [ "clap", "console", "debugid", + "dirs", "exec-harness", "futures", "gimli", @@ -3540,7 +3541,6 @@ dependencies = [ "bincode", "codspeed-divan-compat", "itertools 0.14.0", - "libc", "log", "rand 0.8.5", "rmp", diff --git a/Cargo.toml b/Cargo.toml index 4d4c210e..fee0a574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ shell-words = "1.1.0" rmp-serde = "1.3.0" uuid = { version = "1.21.0", features = ["v4"] } which = "8.0.2" +dirs = "6.0.0" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.17.0" @@ -129,6 +130,11 @@ lto = "thin" strip = true [package.metadata.dist] -targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-musl", + "x86_64-unknown-linux-musl", +] +binaries.aarch64-apple-darwin = ["codspeed"] binaries.aarch64-unknown-linux-musl = ["codspeed"] binaries.x86_64-unknown-linux-musl = ["codspeed"] diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs index 49c7cded..bf65ef7e 100644 --- a/crates/exec-harness/build.rs +++ b/crates/exec-harness/build.rs @@ -62,6 +62,7 @@ fn main() { core_c: instrument_hooks_dir.join("dist/core.c"), includes_dir: instrument_hooks_dir.join("includes"), }; + println!("cargo:rerun-if-changed={}", paths.core_c.display()); paths.check_sources_exist(); build_shared_library(&paths, &preload_constants); } diff --git a/crates/instrument-hooks-bindings/instrument-hooks b/crates/instrument-hooks-bindings/instrument-hooks index 89fb72a0..4376be3e 160000 --- a/crates/instrument-hooks-bindings/instrument-hooks +++ b/crates/instrument-hooks-bindings/instrument-hooks @@ -1 +1 @@ -Subproject commit 89fb72a076ec71c9eca6eee9bca98bada4b4dfb4 +Subproject commit 4376be3e1e2d0e0c82d690bcb8e54bde98b7805c diff --git a/crates/instrument-hooks-bindings/src/lib.rs b/crates/instrument-hooks-bindings/src/lib.rs index c4a12aaa..1a95a4da 100644 --- a/crates/instrument-hooks-bindings/src/lib.rs +++ b/crates/instrument-hooks-bindings/src/lib.rs @@ -163,7 +163,7 @@ mod other_impl { pub struct InstrumentHooks; impl InstrumentHooks { - pub fn instance() -> &'static Self { + pub fn instance(_integration: &str, _version: &str) -> &'static Self { static INSTANCE: InstrumentHooks = InstrumentHooks; &INSTANCE } diff --git a/crates/runner-shared/Cargo.toml b/crates/runner-shared/Cargo.toml index 6ca3db3d..07379351 100644 --- a/crates/runner-shared/Cargo.toml +++ b/crates/runner-shared/Cargo.toml @@ -13,7 +13,6 @@ itertools = { workspace = true } log = { workspace = true } rmp = "0.8.14" rmp-serde = "1.3.0" -libc = { workspace = true } zstd = "0.13" [dev-dependencies] diff --git a/crates/runner-shared/src/artifacts/memtrack.rs b/crates/runner-shared/src/artifacts/memtrack.rs index 64c90e82..15cf2d09 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -1,5 +1,6 @@ -use libc::pid_t; use serde::{Deserialize, Serialize}; + +use crate::pid_t; use std::io::{BufReader, BufWriter, Read, Write}; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/runner-shared/src/artifacts/mod.rs b/crates/runner-shared/src/artifacts/mod.rs index 6df5ff35..6611d150 100644 --- a/crates/runner-shared/src/artifacts/mod.rs +++ b/crates/runner-shared/src/artifacts/mod.rs @@ -1,7 +1,8 @@ use std::io::BufReader; -use libc::pid_t; use log::debug; + +use crate::pid_t; use serde::{Deserialize, Serialize}; mod execution_timestamps; diff --git a/crates/runner-shared/src/lib.rs b/crates/runner-shared/src/lib.rs index 61e804de..0f771b71 100644 --- a/crates/runner-shared/src/lib.rs +++ b/crates/runner-shared/src/lib.rs @@ -6,3 +6,7 @@ pub mod module_symbols; pub mod perf_event; pub mod unwind_data; pub mod walltime_results; + +/// Process ID type, equivalent to `libc::pid_t` on Unix. +#[allow(non_camel_case_types)] +pub type pid_t = i32; diff --git a/crates/runner-shared/src/metadata.rs b/crates/runner-shared/src/metadata.rs index b5c7d46c..d9b18bd2 100644 --- a/crates/runner-shared/src/metadata.rs +++ b/crates/runner-shared/src/metadata.rs @@ -1,6 +1,7 @@ use anyhow::Context; -use libc::pid_t; use serde::{Deserialize, Serialize}; + +use crate::pid_t; use std::collections::HashMap; use std::io::BufWriter; use std::path::Path; diff --git a/scripts/sync-to-local.ps1 b/scripts/sync-to-local.ps1 new file mode 100644 index 00000000..58699ca9 --- /dev/null +++ b/scripts/sync-to-local.ps1 @@ -0,0 +1,16 @@ +# Syncs the repo from the shared folder (Z:\) to a local working copy. +# +# Usage: powershell -ExecutionPolicy Bypass -File Z:\scripts\sync-to-local.ps1 + +$ErrorActionPreference = "Stop" + +$syncs = @( + @{ SRC = "Z:\codspeed"; DST = "C:\Users\vboxuser\codspeed" }, + @{ SRC = "Z:\codspeed-rust"; DST = "C:\Users\vboxuser\codspeed-rust" } +) + +foreach ($sync in $syncs) { + # /MIR = mirror (copy + delete extras), /XD = exclude dirs, /MT = multi-threaded + robocopy $sync.SRC $sync.DST /MIR /MT /XD .git target target_win node_modules /NFL /NDL /NJH /NJS /NC /NS + Write-Host "Synced $($sync.SRC) -> $($sync.DST)" +} diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 19559dbe..db1d8a9d 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -1,4 +1,4 @@ -use crate::executor::{ToolInstallStatus, get_all_executors}; +use crate::executor::{ExecutorSupport, ToolInstallStatus, get_all_executors}; use crate::prelude::*; use crate::system::SystemInfo; use clap::{Args, Subcommand}; @@ -22,10 +22,7 @@ enum SetupCommands { pub async fn run(args: SetupArgs, setup_cache_dir: Option<&Path>) -> Result<()> { match args.command { None => setup(setup_cache_dir).await, - Some(SetupCommands::Status) => { - status(); - Ok(()) - } + Some(SetupCommands::Status) => status(), } } @@ -34,41 +31,81 @@ async fn setup(setup_cache_dir: Option<&Path>) -> Result<()> { let executors = get_all_executors(); start_group!("Setting up the environment for all executors"); for executor in executors { - info!( - "Setting up the environment for the executor: {}", - executor.name() - ); - executor.setup(&system_info, setup_cache_dir).await?; + match executor.support_level(&system_info) { + ExecutorSupport::Unsupported => { + info!( + "Skipping setup for the {} executor: not supported on {}", + executor.name(), + system_info.os + ); + } + ExecutorSupport::RequiresManualInstallation => { + info!( + "Skipping automatic setup for the {} executor on {}; install required tooling manually.", + executor.name(), + system_info.os + ); + } + ExecutorSupport::FullySupported => { + info!( + "Setting up the environment for the executor: {}", + executor.name() + ); + executor.setup(&system_info, setup_cache_dir).await?; + } + } } info!("Environment setup completed"); end_group!(); Ok(()) } -pub fn status() { +pub fn status() -> Result<()> { + let system_info = SystemInfo::new()?; info!("{}", style("Tools").bold()); for executor in get_all_executors() { - let tool_status = executor.tool_status(); - match &tool_status.status { - ToolInstallStatus::Installed { version } => { - info!(" {} {} ({})", check_mark(), tool_status.tool_name, version); - } - ToolInstallStatus::IncorrectVersion { version, message } => { - info!( - " {} {} ({}, {})", - warn_mark(), - tool_status.tool_name, - version, - message - ); - } - ToolInstallStatus::NotInstalled => { + // Don't probe for tooling that can't be used on this OS anyway. + if executor.support_level(&system_info) == ExecutorSupport::Unsupported { + continue; + } + match executor.tool_status() { + Some(tool_status) => match &tool_status.status { + ToolInstallStatus::Installed { version } => { + info!( + " {} {} executor: {} ({})", + check_mark(), + executor.name(), + tool_status.tool_name, + version + ); + } + ToolInstallStatus::IncorrectVersion { version, message } => { + info!( + " {} {} executor: {} ({}, {})", + warn_mark(), + executor.name(), + tool_status.tool_name, + version, + message + ); + } + ToolInstallStatus::NotInstalled => { + info!( + " {} {} executor: {} (not installed)", + cross_mark(), + executor.name(), + tool_status.tool_name + ); + } + }, + None => { info!( - " {} {} (not installed)", - cross_mark(), - tool_status.tool_name + " {} {} executor: No tool to install", + check_mark(), + executor.name() ); } } } + Ok(()) } diff --git a/src/cli/status.rs b/src/cli/status.rs index b8fe3f90..13a5b1b7 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -22,17 +22,14 @@ pub async fn run(api_client: &CodSpeedAPIClient) -> Result<()> { info!(""); // Setup/tools status - super::setup::status(); + super::setup::status()?; info!(""); // System info info!("{}", style("System").bold()); info!(" codspeed {VERSION}"); let system_info = SystemInfo::new()?; - info!( - " {} {} ({})", - system_info.os, system_info.os_version, system_info.arch - ); + info!(" {} ({})", system_info.os, system_info.arch); info!( " {} ({}C / {}GB)", system_info.cpu_brand, system_info.cpu_cores, system_info.total_memory_gb diff --git a/src/config.rs b/src/config.rs index 1e3fdf94..2816aad2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::{env, fs, path::PathBuf}; +use std::{fs, path::PathBuf}; use crate::prelude::*; use nestify::nest; @@ -28,12 +28,7 @@ nest! { /// If config_name is None, returns ~/.config/codspeed/config.yaml (default) /// If config_name is Some, returns ~/.config/codspeed/{config_name}.yaml fn get_configuration_file_path(config_name: Option<&str>) -> PathBuf { - let config_dir = env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| { - let home = env::var("HOME").expect("HOME env variable not set"); - PathBuf::from(home).join(".config") - }); + let config_dir = dirs::config_dir().expect("Could not determine config directory"); let config_dir = config_dir.join("codspeed"); match config_name { diff --git a/src/executor/helpers/apt.rs b/src/executor/helpers/apt.rs index 00fb1cfc..8d430bdc 100644 --- a/src/executor/helpers/apt.rs +++ b/src/executor/helpers/apt.rs @@ -1,13 +1,13 @@ use super::run_with_sudo::run_with_sudo; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use std::path::Path; use std::process::Command; const METADATA_FILENAME: &str = "./tmp/codspeed-cache-metadata.txt"; fn is_system_compatible(system_info: &SystemInfo) -> bool { - system_info.os == "ubuntu" || system_info.os == "debian" + matches!(system_info.os, SupportedOs::Linux(ref distro) if distro.is_supported()) } /// Installs packages with caching support. diff --git a/src/executor/helpers/env.rs b/src/executor/helpers/env.rs index 09b7c0f3..4ebfbabb 100644 --- a/src/executor/helpers/env.rs +++ b/src/executor/helpers/env.rs @@ -1,4 +1,5 @@ use crate::executor::ExecutorConfig; +#[cfg(unix)] use crate::executor::helpers::{introspected_golang, introspected_nodejs}; use crate::prelude::*; use crate::runner_mode::RunnerMode; @@ -82,17 +83,25 @@ pub fn build_path_env(enable_introspection: bool) -> Result { return Ok(path_env); } - let node_path = introspected_nodejs::setup() - .map_err(|e| anyhow!("failed to setup NodeJS introspection. {e}"))?; - let go_path = introspected_golang::setup() - .map_err(|e| anyhow!("failed to setup Go introspection. {e}"))?; + #[cfg(unix)] + { + let node_path = introspected_nodejs::setup() + .map_err(|e| anyhow!("failed to setup NodeJS introspection. {e}"))?; + let go_path = introspected_golang::setup() + .map_err(|e| anyhow!("failed to setup Go introspection. {e}"))?; - Ok(format!( - "{}:{}:{}", - node_path.to_string_lossy(), - go_path.to_string_lossy(), - path_env, - )) + Ok(format!( + "{}:{}:{}", + node_path.to_string_lossy(), + go_path.to_string_lossy(), + path_env, + )) + } + #[cfg(not(unix))] + { + // Language introspection is not yet supported on non-Unix platforms + unimplemented!(); + } } pub fn is_codspeed_debug_enabled() -> bool { diff --git a/src/executor/helpers/harvest_perf_maps_for_pids.rs b/src/executor/helpers/harvest_perf_maps_for_pids.rs index 4ffda881..76b48539 100644 --- a/src/executor/helpers/harvest_perf_maps_for_pids.rs +++ b/src/executor/helpers/harvest_perf_maps_for_pids.rs @@ -5,7 +5,7 @@ use tokio::fs; pub async fn harvest_perf_maps_for_pids( profile_folder: &Path, - pids: &HashSet, + pids: &HashSet, ) -> Result<()> { let perf_maps = pids .iter() diff --git a/src/executor/helpers/introspected_golang/go.sh b/src/executor/helpers/introspected_golang/go.sh index 6aba393d..5601d136 100755 --- a/src/executor/helpers/introspected_golang/go.sh +++ b/src/executor/helpers/introspected_golang/go.sh @@ -18,8 +18,10 @@ if [ "${CODSPEED_RUNNER_MODE:-}" != "walltime" ]; then exit 1 fi -# Find the real go binary, so that we don't end up in infinite recursion -REAL_GO=$(which -a go | grep -v "$(realpath "$0")" | head -1) +# Find the real go binary by removing our directory from PATH (same approach as node.sh) +ORIGINAL_PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "codspeed_introspected_go" | tr "\n" ":") +REAL_GO=$(env PATH="$ORIGINAL_PATH" which go 2>/dev/null || true) +debug_log "Real go path: $REAL_GO" if [ -z "$REAL_GO" ]; then echo "ERROR: Could not find real go binary" >&2 exit 1 diff --git a/src/executor/helpers/mod.rs b/src/executor/helpers/mod.rs index 3e433c7c..3c06f768 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -3,8 +3,11 @@ pub mod command; pub mod detect_executable; pub mod env; pub mod get_bench_command; +#[cfg(unix)] pub mod harvest_perf_maps_for_pids; +#[cfg(unix)] pub mod introspected_golang; +#[cfg(unix)] pub mod introspected_nodejs; pub mod profile_folder; pub mod run_command_with_log_pipe; diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index d5c8320e..03f56aaa 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -1,4 +1,5 @@ use crate::executor::ExecutorName; +use crate::executor::ExecutorSupport; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::get_base_injected_env; @@ -11,7 +12,7 @@ use crate::executor::{ExecutionContext, Executor}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; use crate::runner_mode::RunnerMode; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use async_trait::async_trait; use ipc_channel::ipc; use memtrack::MemtrackIpcClient; @@ -72,8 +73,15 @@ impl Executor for MemoryExecutor { ExecutorName::Memory } - fn tool_status(&self) -> ToolStatus { - get_memtrack_status() + fn tool_status(&self) -> Option { + Some(get_memtrack_status()) + } + + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(_) => ExecutorSupport::FullySupported, + SupportedOs::Macos { .. } => ExecutorSupport::Unsupported, + } } async fn setup( diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 3cee058e..7ff2076b 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -4,11 +4,13 @@ pub mod config; mod execution_context; mod helpers; mod interfaces; +#[cfg(unix)] mod memory; pub mod orchestrator; mod shared; #[cfg(test)] mod tests; +#[cfg(unix)] mod valgrind; mod wall_time; @@ -22,9 +24,7 @@ pub use execution_context::ExecutionContext; pub use interfaces::ExecutorName; pub use orchestrator::Orchestrator; -use memory::executor::MemoryExecutor; use std::path::Path; -use valgrind::executor::ValgrindExecutor; use wall_time::executor::WallTimeExecutor; impl Display for RunnerMode { @@ -41,23 +41,48 @@ impl Display for RunnerMode { pub const EXECUTOR_TARGET: &str = "executor"; -pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { - match mode { - #[allow(deprecated)] - RunnerMode::Instrumentation | RunnerMode::Simulation => Box::new(ValgrindExecutor), - RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), - RunnerMode::Memory => Box::new(MemoryExecutor), +#[cfg(unix)] +mod platform { + use super::*; + use memory::executor::MemoryExecutor; + use valgrind::executor::ValgrindExecutor; + + pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { + match mode { + #[allow(deprecated)] + RunnerMode::Instrumentation | RunnerMode::Simulation => Box::new(ValgrindExecutor), + RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), + RunnerMode::Memory => Box::new(MemoryExecutor), + } + } + + pub fn get_all_executors() -> Vec> { + vec![ + Box::new(ValgrindExecutor), + Box::new(WallTimeExecutor::new()), + Box::new(MemoryExecutor), + ] } } -pub fn get_all_executors() -> Vec> { - vec![ - Box::new(ValgrindExecutor), - Box::new(WallTimeExecutor::new()), - Box::new(MemoryExecutor), - ] +#[cfg(not(unix))] +mod platform { + use super::*; + + pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { + match mode { + RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), + _ => panic!("{mode} mode is not supported on this platform"), + } + } + + pub fn get_all_executors() -> Vec> { + vec![Box::new(WallTimeExecutor::new())] + } } +pub use platform::{get_all_executors, get_executor_from_mode}; + /// Installation status of a tool required by an executor. pub struct ToolStatus { pub tool_name: String, @@ -73,12 +98,28 @@ pub enum ToolInstallStatus { NotInstalled, } +/// How well a given executor runs on a given [`SupportedOs`]. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum ExecutorSupport { + /// The executor cannot run on this OS at all — `run_executor` hard-bails. + Unsupported, + /// The executor runs on this OS, but the user is responsible for installing the required tooling themselves. + RequiresManualInstallation, + /// The executor runs on this OS and `setup()` knows how to auto-install tooling. + FullySupported, +} + #[async_trait(?Send)] pub trait Executor { fn name(&self) -> ExecutorName; /// Report the installation status of the tool(s) this executor depends on. - fn tool_status(&self) -> ToolStatus; + fn tool_status(&self) -> Option; + + /// Declare how well this executor runs on the host system. Drives whether `setup()` is invoked + /// (only when [`ExecutorSupport::FullySupported`]) and whether we bail out of running the + /// executor at all (on [`ExecutorSupport::Unsupported`]). + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport; async fn setup( &self, @@ -107,11 +148,24 @@ pub async fn run_executor( execution_context: &ExecutionContext, setup_cache_dir: Option<&Path>, ) -> Result<()> { - if !execution_context.config.skip_setup { - executor - .setup(&orchestrator.system_info, setup_cache_dir) - .await?; + match executor.support_level(&orchestrator.system_info) { + ExecutorSupport::Unsupported => { + bail!( + "The {} executor is not supported on {}", + executor.name(), + orchestrator.system_info.os + ); + } + ExecutorSupport::RequiresManualInstallation | ExecutorSupport::FullySupported => { + if !execution_context.config.skip_setup { + executor + .setup(&orchestrator.system_info, setup_cache_dir) + .await?; + } + } + } + if !execution_context.config.skip_setup { // TODO: refactor and move directly in the Instruments struct as a `setup` method if execution_context.config.instruments.is_mongodb_enabled() { install_mongodb_tracer().await?; diff --git a/src/executor/shared/fifo.rs b/src/executor/shared/fifo.rs index 39b9533b..577f7bed 100644 --- a/src/executor/shared/fifo.rs +++ b/src/executor/shared/fifo.rs @@ -275,7 +275,7 @@ impl RunnerFifo { } } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; use std::time::Duration; diff --git a/src/executor/shared/mod.rs b/src/executor/shared/mod.rs index 2badf406..9e1bab42 100644 --- a/src/executor/shared/mod.rs +++ b/src/executor/shared/mod.rs @@ -1 +1,2 @@ +#[cfg(unix)] pub mod fifo; diff --git a/src/executor/tests.rs b/src/executor/tests.rs index db982741..fe38e3a5 100644 --- a/src/executor/tests.rs +++ b/src/executor/tests.rs @@ -146,6 +146,7 @@ async fn acquire_bpf_instrumentation_lock() -> SemaphorePermit<'static> { semaphore.acquire().await.unwrap() } +#[cfg(target_os = "linux")] mod valgrind { use super::*; use crate::executor::valgrind::executor::ValgrindExecutor; @@ -377,6 +378,7 @@ fi } } +#[cfg(target_os = "linux")] #[test_with::env(GITHUB_ACTIONS)] mod memory { use super::*; diff --git a/src/executor/valgrind/executor.rs b/src/executor/valgrind/executor.rs index c81c1046..cfdbe88a 100644 --- a/src/executor/valgrind/executor.rs +++ b/src/executor/valgrind/executor.rs @@ -3,12 +3,12 @@ use std::path::Path; use crate::executor::Executor; use crate::executor::ToolStatus; -use crate::executor::{ExecutionContext, ExecutorName}; +use crate::executor::{ExecutionContext, ExecutorName, ExecutorSupport}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; -use super::setup::{get_valgrind_status, install_valgrind}; +use super::setup::{get_codspeed_valgrind_filename, get_valgrind_status, install_valgrind}; use super::{helpers::perf_maps::harvest_perf_maps, helpers::venv_compat, measure}; pub struct ValgrindExecutor; @@ -19,8 +19,21 @@ impl Executor for ValgrindExecutor { ExecutorName::Valgrind } - fn tool_status(&self) -> ToolStatus { - get_valgrind_status() + fn tool_status(&self) -> Option { + Some(get_valgrind_status()) + } + + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(_) => { + if get_codspeed_valgrind_filename(system_info).is_ok() { + ExecutorSupport::FullySupported + } else { + ExecutorSupport::RequiresManualInstallation + } + } + SupportedOs::Macos { .. } => ExecutorSupport::Unsupported, + } } async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { diff --git a/src/executor/valgrind/helpers/perf_maps.rs b/src/executor/valgrind/helpers/perf_maps.rs index 583ca2f9..5260e50a 100644 --- a/src/executor/valgrind/helpers/perf_maps.rs +++ b/src/executor/valgrind/helpers/perf_maps.rs @@ -7,7 +7,7 @@ use std::path::Path; /// Extracts a PID from a profile output file path. /// /// Supports both Callgrind (`.out`) and Tracegrind (`.tgtrace`) file formats. -fn extract_pid_from_profile_file(path: &Path) -> Option { +fn extract_pid_from_profile_file(path: &Path) -> Option { let ext = path.extension()?.to_str()?; match ext { "out" | "tgtrace" => path.file_stem()?.to_str()?.parse().ok(), diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 0d04c892..391673c5 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -2,7 +2,7 @@ use crate::cli::run::helpers::download_file; use crate::executor::helpers::apt; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{LinuxDistribution, SupportedOs, SystemInfo}; use crate::{ VALGRIND_CODSPEED_DEB_VERSION, VALGRIND_CODSPEED_VERSION, VALGRIND_CODSPEED_VERSION_STRING, }; @@ -10,23 +10,37 @@ use semver::Version; use std::{env, path::Path, process::Command}; use url::Url; -fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { - let (version, architecture) = match ( - system_info.os.as_str(), - system_info.os_version.as_str(), - system_info.arch.as_str(), - ) { - ("ubuntu", "22.04", "x86_64") | ("debian", "12", "x86_64") => ("22.04", "amd64"), - ("ubuntu", "24.04", "x86_64") => ("24.04", "amd64"), - ("ubuntu", "22.04", "aarch64") | ("debian", "12", "aarch64") => ("22.04", "arm64"), - ("ubuntu", "24.04", "aarch64") => ("24.04", "arm64"), +pub(super) fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { + let SupportedOs::Linux(distro) = &system_info.os else { + bail!("Unsupported system"); + }; + + let (deb_ubuntu_version, architecture) = match (distro, system_info.arch.as_str()) { + (LinuxDistribution::Ubuntu { version }, "x86_64") + | (LinuxDistribution::Debian { version }, "x86_64") + if version == "22.04" || version == "12" => + { + ("22.04", "amd64") + } + (LinuxDistribution::Ubuntu { version }, "x86_64") if version == "24.04" => { + ("24.04", "amd64") + } + (LinuxDistribution::Ubuntu { version }, "aarch64") + | (LinuxDistribution::Debian { version }, "aarch64") + if version == "22.04" || version == "12" => + { + ("22.04", "arm64") + } + (LinuxDistribution::Ubuntu { version }, "aarch64") if version == "24.04" => { + ("24.04", "arm64") + } _ => bail!("Unsupported system"), }; Ok(format!( "valgrind_{}_ubuntu-{}_{}.deb", VALGRIND_CODSPEED_DEB_VERSION.as_str(), - version, + deb_ubuntu_version, architecture )) } @@ -175,8 +189,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "22.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "22.04".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -189,8 +204,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu_24() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "24.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "24.04".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -203,8 +219,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_debian() { let system_info = SystemInfo { - os: "debian".to_string(), - os_version: "12".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Debian { + version: "12".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -217,8 +234,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu_arm() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "22.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "22.04".into(), + }), arch: "aarch64".to_string(), ..SystemInfo::test() }; @@ -228,6 +246,28 @@ mod tests { ); } + #[test] + fn test_codspeed_valgrind_filename_unsupported_os() { + let system_info = SystemInfo { + os: SupportedOs::Macos { + version: "14.0".into(), + }, + ..SystemInfo::test() + }; + assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + } + + #[test] + fn test_codspeed_valgrind_filename_unsupported_distro() { + let system_info = SystemInfo { + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "20.04".into(), + }), + ..SystemInfo::test() + }; + assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + } + #[test] fn test_parse_valgrind_codspeed_version_with_prefix() { let version = parse_valgrind_codspeed_version("valgrind-3.25.1.codspeed").unwrap(); diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index c317f266..66f5e6ff 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -1,21 +1,18 @@ use super::helpers::validate_walltime_results; -use super::perf::PerfRunner; +use super::isolation::wrap_with_isolation; +use super::platform::WalltimePlatform; use crate::executor::Executor; use crate::executor::ExecutorConfig; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; -use crate::executor::helpers::env::{ - build_path_env, get_base_injected_env, is_codspeed_debug_enabled, -}; +use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; -use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; use crate::executor::helpers::run_with_env::wrap_with_env; -use crate::executor::helpers::run_with_sudo::wrap_with_sudo; -use crate::executor::{ExecutionContext, ExecutorName}; +use crate::executor::{ExecutionContext, ExecutorName, ExecutorSupport}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; use crate::runner_mode::RunnerMode; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use async_trait::async_trait; use std::fs::canonicalize; use std::io::Write; @@ -73,13 +70,13 @@ impl Drop for HookScriptsGuard { } pub struct WallTimeExecutor { - perf: Option, + platform: WalltimePlatform, } impl WallTimeExecutor { pub fn new() -> Self { Self { - perf: cfg!(target_os = "linux").then(PerfRunner::new), + platform: WalltimePlatform::new(), } } @@ -105,28 +102,12 @@ impl WallTimeExecutor { bench_cmd.arg(script_file.path()); let (mut bench_cmd, env_file) = wrap_with_env(bench_cmd, &extra_env)?; - let mut cmd_builder = CommandBuilder::new("systemd-run"); if let Some(cwd) = &config.working_directory { let abs_cwd = canonicalize(cwd)?; - cmd_builder.current_dir(abs_cwd); + bench_cmd.current_dir(abs_cwd); } - if !is_codspeed_debug_enabled() { - cmd_builder.arg("--quiet"); - } - // Remarks: - // - We're using --scope so that perf is able to capture the events of the benchmark process. - // - We can't user `--user` here because we need to run in `codspeed.slice`, otherwise we'd run in - // user.slice` (which is isolated). We can use `--gid` and `--uid` to run the command as the current user. - // - We must use `bash` here instead of `sh` since `source` isn't available when symlinked to `dash`. - // - We have to pass the environment variables because `--scope` only inherits the system and not the user environment variables. - cmd_builder.arg("--slice=codspeed.slice"); - cmd_builder.arg("--scope"); - cmd_builder.arg("--same-dir"); - cmd_builder.arg(format!("--uid={}", nix::unistd::Uid::current().as_raw())); - cmd_builder.arg(format!("--gid={}", nix::unistd::Gid::current().as_raw())); - cmd_builder.args(["--"]); - - bench_cmd.wrap_with(cmd_builder); + + let bench_cmd = wrap_with_isolation(bench_cmd)?; Ok((env_file, script_file, bench_cmd)) } @@ -138,16 +119,20 @@ impl Executor for WallTimeExecutor { ExecutorName::WallTime } - fn tool_status(&self) -> ToolStatus { - super::perf::setup::get_perf_status() + fn tool_status(&self) -> Option { + self.platform.tool_status() } - async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { - if self.perf.is_some() { - return PerfRunner::setup_environment(system_info, setup_cache_dir).await; + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(distro) if distro.is_supported() => ExecutorSupport::FullySupported, + SupportedOs::Macos { .. } => ExecutorSupport::FullySupported, + SupportedOs::Linux(_) => ExecutorSupport::RequiresManualInstallation, } + } - Ok(()) + async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { + self.platform.setup(system_info, setup_cache_dir).await } async fn run( @@ -160,20 +145,10 @@ impl Executor for WallTimeExecutor { let (_env_file, _script_file, cmd_builder) = WallTimeExecutor::walltime_bench_cmd(&execution_context.config, execution_context)?; - if let Some(perf) = &self.perf - && execution_context.config.enable_perf - { - perf.run( - cmd_builder, - &execution_context.config, - &execution_context.profile_folder, - ) + + self.platform + .run_bench_cmd(cmd_builder, execution_context) .await - } else { - let cmd = wrap_with_sudo(cmd_builder)?.build(); - debug!("cmd: {cmd:?}"); - run_command_with_log_pipe(cmd).await - } }; let status = status.map_err(|e| anyhow!("failed to execute the benchmark process. {e}"))?; @@ -189,12 +164,7 @@ impl Executor for WallTimeExecutor { async fn teardown(&self, execution_context: &ExecutionContext) -> Result<()> { debug!("Copying files to the profile folder"); - if let Some(perf) = &self.perf - && execution_context.config.enable_perf - { - perf.save_files_to(&execution_context.profile_folder) - .await?; - } + self.platform.teardown(execution_context).await?; validate_walltime_results( &execution_context.profile_folder, @@ -205,7 +175,7 @@ impl Executor for WallTimeExecutor { } } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests { use tempfile::NamedTempFile; diff --git a/src/executor/wall_time/isolation.rs b/src/executor/wall_time/isolation.rs new file mode 100644 index 00000000..37c41ee9 --- /dev/null +++ b/src/executor/wall_time/isolation.rs @@ -0,0 +1,43 @@ +use crate::executor::helpers::command::CommandBuilder; +use crate::prelude::*; + +/// Run the benchmark command in an isolated process scope. +/// +/// On Linux, the command is wrapped with `systemd-run --scope` so it runs inside the +/// `codspeed.slice` cgroup (required for perf to capture the full process tree). +/// +/// Remarks: +/// - We're using `--scope` so that perf is able to capture the events of the benchmark process. +/// - We can't use `--user` here because we need to run in `codspeed.slice`, otherwise we'd run in +/// `user.slice` (which is isolated). We use `--uid` and `--gid` to keep running as the current +/// user. +/// - `--scope` only inherits the system environment, so the caller is expected to have already +/// forwarded the relevant variables (via `wrap_with_env`). +/// - The caller is expected to have already set the working directory on `bench_cmd`; it will be +/// propagated to `systemd-run` via [`CommandBuilder::wrap_with`], and `--same-dir` makes the +/// spawned scope inherit it. +#[cfg(target_os = "linux")] +pub fn wrap_with_isolation(mut bench_cmd: CommandBuilder) -> Result { + use crate::executor::helpers::env::is_codspeed_debug_enabled; + + let mut cmd_builder = CommandBuilder::new("systemd-run"); + if !is_codspeed_debug_enabled() { + cmd_builder.arg("--quiet"); + } + cmd_builder.arg("--slice=codspeed.slice"); + cmd_builder.arg("--scope"); + cmd_builder.arg("--same-dir"); + cmd_builder.arg(format!("--uid={}", nix::unistd::Uid::current().as_raw())); + cmd_builder.arg(format!("--gid={}", nix::unistd::Gid::current().as_raw())); + cmd_builder.args(["--"]); + + bench_cmd.wrap_with(cmd_builder); + Ok(bench_cmd) +} + +/// Dummy implementation on non-Linux platforms: the benchmark command is returned as-is. +// TODO(COD-2513): implement an equivalent process-isolation mechanism on macOS +#[cfg(not(target_os = "linux"))] +pub fn wrap_with_isolation(bench_cmd: CommandBuilder) -> Result { + Ok(bench_cmd) +} diff --git a/src/executor/wall_time/mod.rs b/src/executor/wall_time/mod.rs index b790b6ac..ddaf7849 100644 --- a/src/executor/wall_time/mod.rs +++ b/src/executor/wall_time/mod.rs @@ -1,3 +1,6 @@ pub mod executor; pub mod helpers; +pub mod isolation; +#[cfg(unix)] pub mod perf; +mod platform; diff --git a/src/executor/wall_time/perf/debug_info.rs b/src/executor/wall_time/perf/debug_info.rs index 5e252bb3..5109044b 100644 --- a/src/executor/wall_time/perf/debug_info.rs +++ b/src/executor/wall_time/perf/debug_info.rs @@ -127,7 +127,9 @@ pub fn debug_info_by_path( .collect() } -#[cfg(test)] +// These tests parse Linux ELF binaries from `testdata/`; gate them on Linux so they're not +// attempted (and don't abort the test process) when running on macOS. +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/src/executor/wall_time/perf/jit_dump.rs b/src/executor/wall_time/perf/jit_dump.rs index f306de08..50f10213 100644 --- a/src/executor/wall_time/perf/jit_dump.rs +++ b/src/executor/wall_time/perf/jit_dump.rs @@ -118,7 +118,7 @@ impl JitDump { /// Unwind data is generated as a list pub async fn save_symbols_and_harvest_unwind_data_for_pids( profile_folder: &Path, - pids: &HashSet, + pids: &HashSet, ) -> Result>> { let mut jit_unwind_data_by_path = HashMap::new(); diff --git a/src/executor/wall_time/perf/module_symbols.rs b/src/executor/wall_time/perf/module_symbols.rs index c9d8fb1f..240a2602 100644 --- a/src/executor/wall_time/perf/module_symbols.rs +++ b/src/executor/wall_time/perf/module_symbols.rs @@ -177,7 +177,7 @@ impl ModuleSymbols { } } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/src/executor/wall_time/perf/parse_perf_file.rs b/src/executor/wall_time/perf/parse_perf_file.rs index 9f5b0fd8..932cbff0 100644 --- a/src/executor/wall_time/perf/parse_perf_file.rs +++ b/src/executor/wall_time/perf/parse_perf_file.rs @@ -1,11 +1,11 @@ use super::module_symbols::ModuleSymbols; use super::unwind_data::unwind_data_from_elf; use crate::prelude::*; -use libc::pid_t; use linux_perf_data::PerfFileReader; use linux_perf_data::PerfFileRecord; use linux_perf_data::linux_perf_event_reader::EventRecord; use linux_perf_data::linux_perf_event_reader::RecordType; +use runner_shared::pid_t; use runner_shared::unwind_data::ProcessUnwindData; use runner_shared::unwind_data::UnwindData; use std::collections::HashMap; diff --git a/src/executor/wall_time/perf/save_artifacts.rs b/src/executor/wall_time/perf/save_artifacts.rs index e5b1d572..7ef0720c 100644 --- a/src/executor/wall_time/perf/save_artifacts.rs +++ b/src/executor/wall_time/perf/save_artifacts.rs @@ -3,10 +3,10 @@ use crate::executor::wall_time::perf::debug_info::debug_info_by_path; use crate::executor::wall_time::perf::naming; use crate::executor::wall_time::perf::parse_perf_file::LoadedModule; use crate::prelude::*; -use libc::pid_t; use rayon::prelude::*; use runner_shared::debug_info::{MappedProcessDebugInfo, ModuleDebugInfo}; use runner_shared::module_symbols::MappedProcessModuleSymbols; +use runner_shared::pid_t; use runner_shared::unwind_data::{MappedProcessUnwindData, ProcessUnwindData, UnwindData}; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/src/executor/wall_time/perf/unwind_data.rs b/src/executor/wall_time/perf/unwind_data.rs index 3b538309..ae39e69d 100644 --- a/src/executor/wall_time/perf/unwind_data.rs +++ b/src/executor/wall_time/perf/unwind_data.rs @@ -87,7 +87,7 @@ pub fn unwind_data_from_elf( Ok((v3, mapping)) } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/src/executor/wall_time/platform/default.rs b/src/executor/wall_time/platform/default.rs new file mode 100644 index 00000000..0e630216 --- /dev/null +++ b/src/executor/wall_time/platform/default.rs @@ -0,0 +1,42 @@ +use crate::executor::ExecutionContext; +use crate::executor::ToolStatus; +use crate::executor::helpers::command::CommandBuilder; +use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; +use crate::prelude::*; +use crate::system::SystemInfo; +use std::path::Path; +use std::process::ExitStatus; + +pub struct WalltimePlatform; + +impl WalltimePlatform { + pub fn new() -> Self { + Self + } + + pub fn tool_status(&self) -> Option { + None + } + + pub async fn setup( + &self, + _system_info: &SystemInfo, + _setup_cache_dir: Option<&Path>, + ) -> Result<()> { + Ok(()) + } + + pub async fn run_bench_cmd( + &self, + cmd_builder: CommandBuilder, + _execution_context: &ExecutionContext, + ) -> Result { + let cmd = cmd_builder.build(); + debug!("cmd: {cmd:?}"); + run_command_with_log_pipe(cmd).await + } + + pub async fn teardown(&self, _execution_context: &ExecutionContext) -> Result<()> { + Ok(()) + } +} diff --git a/src/executor/wall_time/platform/mod.rs b/src/executor/wall_time/platform/mod.rs new file mode 100644 index 00000000..13f1b594 --- /dev/null +++ b/src/executor/wall_time/platform/mod.rs @@ -0,0 +1,9 @@ +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::*; + +#[cfg(not(unix))] +mod default; +#[cfg(not(unix))] +pub use default::*; diff --git a/src/executor/wall_time/platform/unix.rs b/src/executor/wall_time/platform/unix.rs new file mode 100644 index 00000000..b45dcbb4 --- /dev/null +++ b/src/executor/wall_time/platform/unix.rs @@ -0,0 +1,70 @@ +use super::super::perf::PerfRunner; +use crate::executor::ExecutionContext; +use crate::executor::ToolStatus; +use crate::executor::helpers::command::CommandBuilder; +use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; +use crate::executor::helpers::run_with_sudo::wrap_with_sudo; +use crate::prelude::*; +use crate::system::SystemInfo; +use std::path::Path; +use std::process::ExitStatus; + +pub struct WalltimePlatform { + perf: Option, +} + +impl WalltimePlatform { + pub fn new() -> Self { + Self { + perf: cfg!(target_os = "linux").then(PerfRunner::new), + } + } + + pub fn tool_status(&self) -> Option { + self.perf + .as_ref() + .map(|_| super::super::perf::setup::get_perf_status()) + } + + pub async fn setup( + &self, + system_info: &SystemInfo, + setup_cache_dir: Option<&Path>, + ) -> Result<()> { + if self.perf.is_some() { + return PerfRunner::setup_environment(system_info, setup_cache_dir).await; + } + Ok(()) + } + + pub async fn run_bench_cmd( + &self, + cmd_builder: CommandBuilder, + execution_context: &ExecutionContext, + ) -> Result { + if let Some(perf) = &self.perf + && execution_context.config.enable_perf + { + perf.run( + cmd_builder, + &execution_context.config, + &execution_context.profile_folder, + ) + .await + } else { + let cmd = wrap_with_sudo(cmd_builder)?.build(); + debug!("cmd: {cmd:?}"); + run_command_with_log_pipe(cmd).await + } + } + + pub async fn teardown(&self, execution_context: &ExecutionContext) -> Result<()> { + if let Some(perf) = &self.perf + && execution_context.config.enable_perf + { + perf.save_files_to(&execution_context.profile_folder) + .await?; + } + Ok(()) + } +} diff --git a/src/runner_mode/shell_session.rs b/src/runner_mode/shell_session.rs index 46586563..a371f7b9 100644 --- a/src/runner_mode/shell_session.rs +++ b/src/runner_mode/shell_session.rs @@ -1,6 +1,6 @@ use super::RunnerMode; use crate::prelude::*; -use libc::pid_t; +use runner_shared::pid_t; use std::path::Path; use std::path::PathBuf; use std::sync::OnceLock; diff --git a/src/system/check.rs b/src/system/check.rs index 9aa35b0d..d291606c 100644 --- a/src/system/check.rs +++ b/src/system/check.rs @@ -1,56 +1,8 @@ -use std::collections::HashSet; -use std::sync::LazyLock; - use crate::prelude::*; use super::SystemInfo; -static SUPPORTED_SYSTEMS: LazyLock> = - LazyLock::new(|| { - HashSet::from([ - ("ubuntu", "22.04", "x86_64"), - ("ubuntu", "24.04", "x86_64"), - ("ubuntu", "22.04", "aarch64"), - ("ubuntu", "24.04", "aarch64"), - ("debian", "12", "x86_64"), - ("debian", "12", "aarch64"), - ]) - }); - -/// Checks if the provided system info is supported -/// -/// Supported systems: -/// - Ubuntu 20.04 x86_64 -/// - Ubuntu 22.04 x86_64 and aarch64 -/// - Debian 11 x86_64 -/// - Debian 12 x86_64 pub fn check_system(system_info: &SystemInfo) -> Result<()> { debug!("System info: {system_info:#?}"); - - let system_tuple = ( - system_info.os.as_str(), - system_info.os_version.as_str(), - system_info.arch.as_str(), - ); - - if SUPPORTED_SYSTEMS.contains(&system_tuple) { - return Ok(()); - } - - match system_info.arch.as_str() { - "x86_64" | "aarch64" => { - warn!( - "Unofficially supported system: {} {}. Continuing with best effort support.", - system_info.os, system_info.os_version - ); - return Ok(()); - } - _ => {} - } - - bail!( - "Unsupported system: {} {}", - system_info.os, - system_info.os_version - ); + Ok(()) } diff --git a/src/system/info.rs b/src/system/info.rs index d62fec01..51f1bde4 100644 --- a/src/system/info.rs +++ b/src/system/info.rs @@ -1,9 +1,10 @@ use std::process::Command; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; use crate::prelude::*; +use crate::system::os::SupportedOs; fn get_user() -> Result { let user_output = Command::new("whoami") @@ -17,11 +18,12 @@ fn get_user() -> Result { Ok(output_str.trim().to_string()) } -#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Clone)] +#[derive(Eq, PartialEq, Hash, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SystemInfo { - pub os: String, - pub os_version: String, + /// Flattened to the `os` and `osVersion` fields on the wire via [`SupportedOs`]'s serde impl. + #[serde(flatten)] + pub os: SupportedOs, pub arch: String, pub host: String, pub user: String, @@ -37,8 +39,9 @@ pub struct SystemInfo { impl SystemInfo { pub fn test() -> Self { SystemInfo { - os: "ubuntu".to_string(), - os_version: "20.04".to_string(), + os: SupportedOs::Linux(crate::system::LinuxDistribution::Ubuntu { + version: "20.04".into(), + }), arch: "x86_64".to_string(), host: "host".to_string(), user: "user".to_string(), @@ -96,8 +99,7 @@ fn get_cpu_flags() -> Vec { impl SystemInfo { pub fn new() -> Result { - let os = System::distribution_id(); - let os_version = System::os_version().ok_or(anyhow!("Failed to get OS version"))?; + let os = SupportedOs::from_os(std::env::consts::OS)?; let arch = System::cpu_arch(); let user = get_user()?; let host = System::host_name().ok_or(anyhow!("Failed to get host name"))?; @@ -133,7 +135,6 @@ impl SystemInfo { Ok(SystemInfo { os, - os_version, arch, host, user, diff --git a/src/system/mod.rs b/src/system/mod.rs index df68d6a9..219cd831 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -1,5 +1,7 @@ mod check; mod info; +mod os; pub use check::check_system; pub use info::SystemInfo; +pub use os::{LinuxDistribution, SupportedOs}; diff --git a/src/system/os.rs b/src/system/os.rs new file mode 100644 index 00000000..b83cb22b --- /dev/null +++ b/src/system/os.rs @@ -0,0 +1,140 @@ +use std::fmt::{self, Display}; + +use serde::{Deserialize, Serialize}; +use sysinfo::System; + +use crate::prelude::*; +/// Typed representation of the host operating system. +/// +/// Only operating systems that CodSpeed can run on are represented here. +/// Construction via [`SupportedOs::from_current_system`] bails on unsupported platforms +#[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize)] +#[serde(into = "SupportedOsSerde")] +pub enum SupportedOs { + Linux(LinuxDistribution), + Macos { version: String }, +} + +impl SupportedOs { + /// Build a [`SupportedOs`] from the given OS family string. + /// Expects `std::env::consts::OS` as input + /// + /// For Linux, the distribution is identified via `sysinfo::System::distribution_id()`. + /// The OS version is read from `sysinfo::System::os_version()`. + pub fn from_os(os: &str) -> Result { + let os_version = System::os_version().ok_or(anyhow!("Failed to get OS version"))?; + match os { + "linux" => { + let os_id = System::distribution_id(); + Ok(Self::Linux(LinuxDistribution::from_id(&os_id, &os_version))) + } + "macos" => Ok(Self::Macos { + version: os_version, + }), + unsupported => bail!("Unsupported operating system: {unsupported}"), + } + } + + /// The distro/OS id as it appears on the wire (matches `sysinfo::System::distribution_id()`). + pub fn id(&self) -> &str { + match self { + Self::Linux(distro) => distro.id(), + Self::Macos { .. } => "macos", + } + } + + pub fn version(&self) -> &str { + match self { + Self::Linux(distro) => distro.version(), + Self::Macos { version } => version, + } + } +} + +impl Display for SupportedOs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.id(), self.version()) + } +} + +/// Flat `{os, osVersion}` shape we emit on the wire as part of `SystemInfo`. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SupportedOsSerde { + os: String, + os_version: String, +} + +impl From for SupportedOsSerde { + fn from(os: SupportedOs) -> Self { + SupportedOsSerde { + os: os.id().to_string(), + os_version: os.version().to_string(), + } + } +} + +/// Linux distribution, identified by the `sysinfo` distribution id. +#[derive(Eq, PartialEq, Hash, Debug, Clone)] +pub enum LinuxDistribution { + Ubuntu { version: String }, + Debian { version: String }, + Other { name: String, version: String }, +} + +impl LinuxDistribution { + /// Build a [`LinuxDistribution`] from the raw `(os_id, version)` strings reported by `sysinfo`. + fn from_id(os_id: &str, version: &str) -> Self { + match os_id { + "ubuntu" => Self::Ubuntu { + version: version.to_string(), + }, + "debian" => Self::Debian { + version: version.to_string(), + }, + _ => Self::Other { + name: os_id.to_string(), + version: version.to_string(), + }, + } + } + + /// The distro id as it appears on the wire (matches `sysinfo::System::distribution_id()`). + pub fn id(&self) -> &str { + match self { + Self::Ubuntu { .. } => "ubuntu", + Self::Debian { .. } => "debian", + Self::Other { name, .. } => name, + } + } + + pub fn version(&self) -> &str { + match self { + Self::Ubuntu { version } | Self::Debian { version } | Self::Other { version, .. } => { + version + } + } + } + + /// Whether this distribution has first-class support (auto-install via apt, prebuilt .debs, etc.). + pub fn is_supported(&self) -> bool { + matches!(self, Self::Ubuntu { .. } | Self::Debian { .. }) + } +} + +impl Display for LinuxDistribution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.id(), self.version()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_os_bails_on_unsupported() { + let err = SupportedOs::from_os("windows").unwrap_err(); + assert_eq!(err.to_string(), "Unsupported operating system: windows"); + } +} diff --git a/src/upload/interfaces.rs b/src/upload/interfaces.rs index c6dc2a8d..e07c4525 100644 --- a/src/upload/interfaces.rs +++ b/src/upload/interfaces.rs @@ -7,7 +7,7 @@ use crate::system::SystemInfo; pub const LATEST_UPLOAD_METADATA_VERSION: u32 = 10; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UploadMetadata { pub repository_provider: RepositoryProvider, @@ -24,7 +24,7 @@ pub struct UploadMetadata { pub run_environment_metadata: RunEnvironmentMetadata, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Runner { pub name: String, diff --git a/src/upload/upload_metadata.rs b/src/upload/upload_metadata.rs index 68cf51d3..fd64758e 100644 --- a/src/upload/upload_metadata.rs +++ b/src/upload/upload_metadata.rs @@ -96,8 +96,12 @@ mod tests { instruments: vec![], executor: ExecutorName::Valgrind, system_info: SystemInfo { - os: "nixos".to_string(), - os_version: "25.11".to_string(), + os: crate::system::SupportedOs::Linux( + crate::system::LinuxDistribution::Other { + name: "nixos".into(), + version: "25.11".into(), + }, + ), arch: "x86_64".to_string(), host: "badlands".to_string(), user: "guillaume".to_string(),