From 7c4b2aad5da640a3b3edba3dcb361ab548023caa Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Thu, 3 Sep 2026 11:12:12 +0200 Subject: [PATCH 1/5] fix(system): don't fail when the distribution reports no OS version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SupportedOs::from_os` read `sysinfo::System::os_version()` before looking at the OS at all and bailed with "Failed to get OS version" when it was absent. That value comes from `VERSION_ID` in `/etc/os-release`, which rolling releases do not ship: on Arch (`ID=archarm`, `BUILD_ID=rolling`) every command aborted before it could determine anything about the host. The version only matters for the distributions we publish packages for, and those all expose one — the rest are already handled by `is_supported()`. So on Linux fall back to `"unknown"` with a `debug!` rather than failing, and keep the hard error on macOS, where the version is always available and is what we report to the API. Refs COD-3072 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VZZqxhWtwHhurnV2hsJ48H --- src/system/os.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/system/os.rs b/src/system/os.rs index b83cb22b5..547df34f4 100644 --- a/src/system/os.rs +++ b/src/system/os.rs @@ -4,6 +4,10 @@ use serde::{Deserialize, Serialize}; use sysinfo::System; use crate::prelude::*; + +/// Placeholder version for distributions that do not report one (rolling releases). +const UNKNOWN_OS_VERSION: &str = "unknown"; + /// Typed representation of the host operating system. /// /// Only operating systems that CodSpeed can run on are represented here. @@ -22,14 +26,21 @@ impl SupportedOs { /// 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(); + // Rolling release distributions (Arch, Gentoo, ...) do not expose a `VERSION_ID` + // in `/etc/os-release`, so `sysinfo` reports no version for them. This is not + // fatal: the version only matters for the distributions we ship packages for, + // which all expose one. + let os_version = System::os_version().unwrap_or_else(|| { + debug!("No OS version reported for distribution {os_id}"); + UNKNOWN_OS_VERSION.to_string() + }); Ok(Self::Linux(LinuxDistribution::from_id(&os_id, &os_version))) } "macos" => Ok(Self::Macos { - version: os_version, + version: System::os_version().ok_or(anyhow!("Failed to get OS version"))?, }), unsupported => bail!("Unsupported operating system: {unsupported}"), } @@ -137,4 +148,13 @@ mod tests { let err = SupportedOs::from_os("windows").unwrap_err(); assert_eq!(err.to_string(), "Unsupported operating system: windows"); } + + #[test] + #[cfg(target_os = "linux")] + fn from_os_succeeds_on_linux_without_version_id() { + // Rolling releases report no version: we must still build a `SupportedOs`. + let os = SupportedOs::from_os("linux").unwrap(); + assert!(matches!(os, SupportedOs::Linux(_))); + assert!(!os.version().is_empty()); + } } From bff90c552b227eea1f7e7afe4757c38e1cfd6c3c Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Thu, 3 Sep 2026 11:38:25 +0200 Subject: [PATCH 2/5] fix(valgrind): accept a manual installation where we ship no package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `install_valgrind` went straight to `apt::install_cached`, which resolves the `valgrind-codspeed` deb for the host through `get_codspeed_valgrind_target`. That mapping only covers the Debian/Ubuntu versions we publish packages for, so on anything else — Arch and other rolling releases, non-apt distributions — it bailed with a bare "Unsupported system" and the run stopped at setup. Yet `ValgrindExecutor::support_level` already reports `RequiresManualInstallation` for those hosts, so the executor was advertising a path that setup refused to take. Mirror that support level in the setup: when no package exists for the host, return early if a valgrind installation is already present, and otherwise fail with an error that says CodSpeed publishes nothing for this distribution and points at valgrind-codspeed for a manual install. The libc debug symbol check is already skipped on non-apt systems, so an existing build is enough. Refs COD-3072 Co-Authored-By: Claude Opus 5 (1M context) --- src/executor/valgrind/setup.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 32832c089..7c96df0f3 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -245,6 +245,26 @@ pub async fn install_valgrind( system_info: &SystemInfo, setup_cache_dir: Option<&Path>, ) -> Result<()> { + // On distributions we do not publish a valgrind-codspeed package for (rolling releases, + // non-apt systems, ...), there is nothing to install automatically: the user brings their own + // build. Accept that installation instead of failing on the package we cannot provide. + if !is_codspeed_valgrind_installation_supported(system_info) { + if is_valgrind_installed(system_info) { + debug!( + "Using the valgrind installation already present on {}", + system_info.os + ); + return Ok(()); + } + + bail!( + "CodSpeed does not publish a valgrind package for {}, so it cannot be installed automatically. \ + Install valgrind-codspeed {} or higher manually, see https://github.com/CodSpeedHQ/valgrind-codspeed", + system_info.os, + VALGRIND_CODSPEED_VERSION_STRING.as_str() + ); + } + apt::install_cached( system_info, setup_cache_dir, From a1fe17787b83b69bbb96cf682c220b64efbf605e Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Thu, 3 Sep 2026 22:41:02 +0200 Subject: [PATCH 3/5] feat(valgrind): build valgrind-codspeed from source as a fallback On the systems we publish no valgrind package for (rolling releases, non-apt distributions, ...), the setup used to give up immediately and ask for a manual installation. Try a best-effort source build instead: check the build toolchain, clone the sources, compile them and install system-wide, only falling back to the manual instructions when any of those steps fails. Co-Authored-By: Claude Opus 5 (1M context) --- src/executor/valgrind/build_from_source.rs | 200 +++++++++++++++++++++ src/executor/valgrind/mod.rs | 1 + src/executor/valgrind/setup.rs | 21 ++- 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/executor/valgrind/build_from_source.rs diff --git a/src/executor/valgrind/build_from_source.rs b/src/executor/valgrind/build_from_source.rs new file mode 100644 index 000000000..d321b2f29 --- /dev/null +++ b/src/executor/valgrind/build_from_source.rs @@ -0,0 +1,200 @@ +//! Best-effort fallback that builds valgrind-codspeed from source, for the +//! systems we do not publish a package for (rolling releases, non-apt +//! distributions, ...). +//! +//! This is deliberately a "best effort": the toolchain needed to build valgrind +//! is not guaranteed to be present, so every failure is reported back to the +//! caller, which falls back to asking for a manual installation. + +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::local_logger::rolling_buffer::{activate_rolling_buffer, deactivate_rolling_buffer}; +use crate::prelude::*; +use crate::system::{SupportedOs, SystemInfo}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::{env, fs}; + +const VALGRIND_CODSPEED_REPOSITORY: &str = "https://github.com/CodSpeedHQ/valgrind-codspeed.git"; + +/// Branch of the valgrind-codspeed repository to build from. +// TODO: switch back to `main` once the self-contained build script has landed there. +const VALGRIND_CODSPEED_BRANCH: &str = "cod-3465-create-a-self-contained-valgrind-build-script"; + +/// Directory name, under the system temporary directory, the sources are cloned into. +const SOURCE_DIR_NAME: &str = "valgrind-codspeed-src"; + +/// Tools required to configure and build valgrind. Each entry lists the +/// interchangeable executables that satisfy the requirement. +const BUILD_DEPENDENCIES: &[&[&str]] = &[ + &["git"], + &["make"], + &["autoconf"], + &["automake"], + &["cc", "gcc", "clang"], +]; + +fn is_executable_available(executable: &str) -> bool { + Command::new("which") + .arg(executable) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + +/// Names of the missing build dependencies, one per unsatisfied requirement. +fn missing_build_dependencies() -> Vec<&'static str> { + BUILD_DEPENDENCIES + .iter() + .filter(|alternatives| { + !alternatives + .iter() + .any(|executable| is_executable_available(executable)) + }) + .map(|alternatives| alternatives[0]) + .collect() +} + +/// Command that installs the build toolchain, for the distributions we can name it for. +/// +/// Valgrind itself needs no development library: it links `-nodefaultlibs` and vendors its only +/// third-party decoder, so an autotools and C toolchain is the whole requirement. +fn build_toolchain_install_hint(system_info: &SystemInfo) -> Option<&'static str> { + let SupportedOs::Linux(distro) = &system_info.os else { + return None; + }; + + // `id` is the `ID` field of /etc/os-release, so derivatives report their own id. + let hint = match distro.id() { + "arch" | "archarm" | "manjaro" | "endeavouros" | "cachyos" => { + "sudo pacman -S --needed base-devel git" + } + "fedora" | "rhel" | "centos" | "rocky" | "almalinux" => { + "sudo dnf install -y @development-tools git" + } + "opensuse-tumbleweed" | "opensuse-leap" | "sles" => { + "sudo zypper install -y -t pattern devel_basis git" + } + "alpine" => "sudo apk add build-base autoconf automake git", + "ubuntu" | "debian" | "linuxmint" | "pop" | "raspbian" => { + "sudo apt-get install -y build-essential autoconf automake git" + } + _ => return None, + }; + Some(hint) +} + +fn parallel_jobs() -> usize { + std::thread::available_parallelism() + .map(|jobs| jobs.get()) + .unwrap_or(1) +} + +fn command_in>(directory: &Path, program: S, args: &[&str]) -> CommandBuilder { + let mut builder = CommandBuilder::new(program); + builder.args(args); + builder.current_dir(directory); + builder +} + +/// Run a build command, piping its output to the logs, and fail on a non-zero exit status. +async fn run_build_command(builder: CommandBuilder) -> Result<()> { + let command_line = builder.as_command_line(); + debug!("Running: {command_line}"); + + let status = run_command_with_log_pipe(builder.build()) + .await + .with_context(|| format!("failed to run `{command_line}`"))?; + + if !status.success() { + bail!("`{command_line}` failed with {status}"); + } + + Ok(()) +} + +/// Clone the sources from scratch, so that a partial or outdated checkout left +/// over by a previous attempt never leaks into the build. +async fn clone_sources() -> Result { + let source_dir = env::temp_dir().join(SOURCE_DIR_NAME); + if source_dir.exists() { + debug!("Removing the previous checkout at {}", source_dir.display()); + fs::remove_dir_all(&source_dir).with_context(|| { + format!( + "failed to remove the previous checkout at {}", + source_dir.display() + ) + })?; + } + + let source_dir_str = source_dir.to_string_lossy().into_owned(); + let mut builder = CommandBuilder::new("git"); + builder.args([ + "clone", + "--depth", + "1", + "--branch", + VALGRIND_CODSPEED_BRANCH, + VALGRIND_CODSPEED_REPOSITORY, + &source_dir_str, + ]); + run_build_command(builder).await?; + + Ok(source_dir) +} + +/// Everything that runs unprivileged: fetching the sources and compiling them. +async fn fetch_and_compile() -> Result { + let source_dir = clone_sources().await?; + + // The scripts are addressed by absolute path: how a relative program path is resolved against + // the working directory of the child is platform specific and unspecified. + run_build_command(command_in(&source_dir, source_dir.join("autogen.sh"), &[])).await?; + run_build_command(command_in(&source_dir, source_dir.join("configure"), &[])).await?; + run_build_command(command_in( + &source_dir, + "make", + &[&format!("-j{}", parallel_jobs())], + )) + .await?; + + Ok(source_dir) +} + +/// Install the freshly built valgrind system-wide. Kept out of the rolling +/// buffer so that a sudo password prompt stays visible to the user. +async fn install_build(source_dir: &Path) -> Result<()> { + let builder = wrap_with_sudo(command_in(source_dir, "make", &["install"]))?; + run_build_command(builder).await +} + +/// Build and install valgrind-codspeed from source. +/// +/// Returns an error describing the first failing step, leaving the caller free +/// to fall back to instructions for a manual installation. +pub(super) async fn build_and_install(system_info: &SystemInfo) -> Result<()> { + let missing_dependencies = missing_build_dependencies(); + if !missing_dependencies.is_empty() { + let missing = missing_dependencies.join(", "); + match build_toolchain_install_hint(system_info) { + Some(hint) => bail!( + "the build toolchain is incomplete ({missing} missing), install it with `{hint}`" + ), + None => bail!("the build toolchain is incomplete ({missing} missing)"), + } + } + + info!("Building valgrind-codspeed from source, this can take a few minutes"); + + activate_rolling_buffer("Building valgrind from source"); + let compilation_result = fetch_and_compile().await; + deactivate_rolling_buffer(); + + let source_dir = compilation_result?; + install_build(&source_dir).await?; + + Ok(()) +} diff --git a/src/executor/valgrind/mod.rs b/src/executor/valgrind/mod.rs index 3db9a666e..371bab43c 100644 --- a/src/executor/valgrind/mod.rs +++ b/src/executor/valgrind/mod.rs @@ -1,3 +1,4 @@ +mod build_from_source; pub mod executor; pub mod helpers; mod measure; diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 7c96df0f3..65d58f63d 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -1,3 +1,4 @@ +use super::build_from_source; use crate::binary_pins::{ Arch, DistroVersion, PinnedBinary, VALGRIND_CODSPEED_ITERATION, VALGRIND_CODSPEED_VERSION, VALGRIND_CODSPEED_VERSION_STRING, ValgrindTarget, @@ -257,8 +258,26 @@ pub async fn install_valgrind( return Ok(()); } + // No package to publish means nothing to install automatically: try to build + // valgrind-codspeed from source instead. This is a best effort, the build toolchain may be + // missing or the build may fail, in which case the user is pointed to a manual installation. + warn!( + "CodSpeed does not publish a valgrind package for {}, falling back to building it from source", + system_info.os + ); + let build_error = match build_from_source::build_and_install(system_info).await { + Ok(()) => { + if is_valgrind_installed(system_info) { + info!("valgrind-codspeed has been built and installed from source"); + return Ok(()); + } + anyhow!("the freshly built valgrind is not usable, see the logs above") + } + Err(error) => error, + }; + bail!( - "CodSpeed does not publish a valgrind package for {}, so it cannot be installed automatically. \ + "CodSpeed does not publish a valgrind package for {}, and building it from source failed: {build_error}. \ Install valgrind-codspeed {} or higher manually, see https://github.com/CodSpeedHQ/valgrind-codspeed", system_info.os, VALGRIND_CODSPEED_VERSION_STRING.as_str() From 944098011cdfa8d6ca78c3363138a282e29a6d74 Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Fri, 4 Sep 2026 10:32:15 +0200 Subject: [PATCH 4/5] refactor(valgrind): scope the libc debug symbol probe to the apt install path `is_valgrind_installed` answered two questions at once: whether a usable valgrind-codspeed is on PATH, and whether the system libc has separate debug symbols. The second belongs to the apt path alone, where the setup cache restores package files without touching dpkg's database and `dpkg -s libc6-dbg` therefore lies (e5587f4). Everywhere else it was neutralised by an `apt::is_system_compatible` early return. But that early return only covers non-apt systems, so on Ubuntu and Debian the probe leaked into the two callers that install nothing: accepting a manual installation, and verifying a source build. Neither installs `libc6-dbg`, so both were rejected on the very distributions where the check applies. The source-build fallback could therefore never succeed on Ubuntu, and a user who followed the resulting "install it manually" instruction was refused again, for a reason the error never named. Split the function in two and apply the probe at the one call site whose decision it belongs to, the `apt::install_cached` idempotency closure, where `apt::is_system_compatible` holds by construction and the early return was dead weight. Missing symbols now warn rather than block, reusing the wording the perf profiler already has for them. Co-Authored-By: Claude Opus 5 (1M context) --- src/executor/valgrind/setup.rs | 49 ++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 65d58f63d..1e7fe32ca 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -226,20 +226,43 @@ fn has_debug_symbols(binary: &Path) -> bool { } } -fn is_valgrind_installed(system_info: &SystemInfo) -> bool { - if !matches!( +/// Whether a valgrind-codspeed recent enough for this runner is on `PATH`. +fn is_valgrind_installed() -> bool { + matches!( get_valgrind_status().status, ToolInstallStatus::Installed { .. } - ) { - return false; - } + ) +} + +/// Whether the system libc has a resolvable separate debug file, as the `libc6-dbg` +/// package provides. +/// +/// Probed by file rather than through dpkg, because the setup cache restores package +/// files onto the root filesystem without touching dpkg's database. Only meaningful +/// where [`system_libc_path`] resolves, so callers must already know they are on the +/// Debian multiarch layout. +fn has_libc_debug_symbols(system_info: &SystemInfo) -> bool { + system_libc_path(system_info).is_some_and(|libc| has_debug_symbols(&libc)) +} +/// Warn, without failing, when valgrind will run against a libc it has no debug +/// symbols for. +/// +/// The symbols sharpen valgrind's output but are not needed to run it, so a missing +/// `libc6-dbg` must not reject an installation we did not package ourselves. Scoped to +/// apt-based systems, the only layout [`system_libc_path`] knows. +fn warn_on_missing_libc_debug_symbols(system_info: &SystemInfo) { if !apt::is_system_compatible(system_info) { - debug!("Skipping libc debug symbol check on non-apt-based system"); - return true; + debug!("Skipping the libc debug symbol check on a non-apt-based system"); + return; } - system_libc_path(system_info).is_some_and(|libc| has_debug_symbols(&libc)) + if !has_libc_debug_symbols(system_info) { + warn!( + "Debug info for the system libc not found. Install libc6-dbg (Debian/Ubuntu) \ + or glibc-debuginfo (Fedora/RHEL) for more accurate valgrind results" + ); + } } pub async fn install_valgrind( @@ -250,11 +273,12 @@ pub async fn install_valgrind( // non-apt systems, ...), there is nothing to install automatically: the user brings their own // build. Accept that installation instead of failing on the package we cannot provide. if !is_codspeed_valgrind_installation_supported(system_info) { - if is_valgrind_installed(system_info) { + if is_valgrind_installed() { debug!( "Using the valgrind installation already present on {}", system_info.os ); + warn_on_missing_libc_debug_symbols(system_info); return Ok(()); } @@ -267,8 +291,9 @@ pub async fn install_valgrind( ); let build_error = match build_from_source::build_and_install(system_info).await { Ok(()) => { - if is_valgrind_installed(system_info) { + if is_valgrind_installed() { info!("valgrind-codspeed has been built and installed from source"); + warn_on_missing_libc_debug_symbols(system_info); return Ok(()); } anyhow!("the freshly built valgrind is not usable, see the logs above") @@ -287,7 +312,9 @@ pub async fn install_valgrind( apt::install_cached( system_info, setup_cache_dir, - || is_valgrind_installed(system_info), + // The libc debug symbols are part of what this path installs, so a cache restore that + // brought back only valgrind must still count as incomplete. + || is_valgrind_installed() && has_libc_debug_symbols(system_info), || async { debug!("Installing valgrind"); let binary = get_codspeed_valgrind_binary(system_info)?; From 2b9c773b05f4787dd6caf8fb278e60a892a5604e Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Fri, 4 Sep 2026 10:32:38 +0200 Subject: [PATCH 5/5] feat(valgrind): ask before building valgrind from source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source-build fallback ran as soon as it could, which spends minutes compiling and then installs system-wide under sudo without the user having agreed to either. Ask first, mirroring the confirmation the walltime executor uses before installing bash: an interactive run gets a `[Y/n]` prompt on stderr, `CODSPEED_VALGRIND_BUILD_FROM_SOURCE` answers it without asking, and a run with no terminal builds anyway, since nobody is there to answer and failing the run outright is the worse outcome. The prompt is wrapped in `suspend_progress_bar` so a spinner cannot overwrite the question, as the sudo password prompt already does. Declining is an outcome, not an error: the three ways this fallback can end without a usable valgrind — declined, failed, built but unusable — now warn and fall through to the same instruction to install manually, instead of being reported as "building it from source failed". Co-Authored-By: Claude Opus 5 (1M context) --- src/executor/valgrind/build_from_source.rs | 86 ++++++++++++++++++++++ src/executor/valgrind/setup.rs | 28 +++---- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/executor/valgrind/build_from_source.rs b/src/executor/valgrind/build_from_source.rs index d321b2f29..437da06ec 100644 --- a/src/executor/valgrind/build_from_source.rs +++ b/src/executor/valgrind/build_from_source.rs @@ -5,13 +5,18 @@ //! This is deliberately a "best effort": the toolchain needed to build valgrind //! is not guaranteed to be present, so every failure is reported back to the //! caller, which falls back to asking for a manual installation. +//! +//! The build is also opt-in rather than automatic, see [`is_wanted`]: it takes +//! minutes and installs system-wide, so an interactive user is asked first. 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::local_logger::rolling_buffer::{activate_rolling_buffer, deactivate_rolling_buffer}; +use crate::local_logger::{IS_TTY, suspend_progress_bar}; use crate::prelude::*; use crate::system::{SupportedOs, SystemInfo}; +use console::Term; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -19,6 +24,10 @@ use std::{env, fs}; const VALGRIND_CODSPEED_REPOSITORY: &str = "https://github.com/CodSpeedHQ/valgrind-codspeed.git"; +/// Environment variable that answers [`is_wanted`] without asking, for CI and any +/// other unattended run that wants the opposite of the default. +const BUILD_FROM_SOURCE_ENV: &str = "CODSPEED_VALGRIND_BUILD_FROM_SOURCE"; + /// Branch of the valgrind-codspeed repository to build from. // TODO: switch back to `main` once the self-contained build script has landed there. const VALGRIND_CODSPEED_BRANCH: &str = "cod-3465-create-a-self-contained-valgrind-build-script"; @@ -171,6 +180,65 @@ async fn install_build(source_dir: &Path) -> Result<()> { run_build_command(builder).await } +/// Whether to build valgrind-codspeed from source, asking the user when we can. +/// +/// Decision, in order: +/// +/// - [`BUILD_FROM_SOURCE_ENV`] set to `true` or `false`: that answer, unconditionally; +/// - not a TTY (CI, unattended runs): build, since nobody is there to answer and +/// failing the run outright is the worse outcome; +/// - otherwise: ask, defaulting to building when the answer is empty. +/// +/// Declining is a legitimate choice, not a failure: the caller then points at a +/// manual installation, which is what happens on a failed build too. +pub(super) fn is_wanted() -> bool { + match env::var(BUILD_FROM_SOURCE_ENV).as_deref() { + Ok("true") => { + debug!("{BUILD_FROM_SOURCE_ENV} is true, building valgrind from source"); + return true; + } + Ok("false") => { + debug!("{BUILD_FROM_SOURCE_ENV} is false, not building valgrind from source"); + return false; + } + Ok(value) => warn!("Ignoring {BUILD_FROM_SOURCE_ENV}={value}, expected `true` or `false`"), + Err(_) => {} + } + + if !*IS_TTY { + debug!("Not attached to a terminal, building valgrind from source without asking"); + return true; + } + + suspend_progress_bar(prompt_for_source_build) +} + +/// Ask whether to build valgrind from source, defaulting to yes on an empty answer. +/// +/// Mirrors the confirmation the walltime executor uses before installing bash: the +/// question goes to stderr so it stays visible whatever the caller does with stdout. +fn prompt_for_source_build() -> bool { + eprintln!( + "CodSpeed can build valgrind-codspeed from source for this system. It clones the sources \ + into a temporary directory, compiles them (a few minutes) and installs them system-wide \ + with sudo. Declining leaves the installation to you, see \ + https://github.com/CodSpeedHQ/valgrind-codspeed" + ); + eprint!("\nBuild valgrind-codspeed from source now? [Y/n] "); + + let line = Term::stderr().read_line().unwrap_or_default(); + let answer = line.trim(); + + let accepted = + answer.is_empty() || answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"); + if !accepted { + info!( + "Skipping the source build. Set {BUILD_FROM_SOURCE_ENV}=true to build without being asked" + ); + } + accepted +} + /// Build and install valgrind-codspeed from source. /// /// Returns an error describing the first failing step, leaving the caller free @@ -198,3 +266,21 @@ pub(super) async fn build_and_install(system_info: &SystemInfo) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Only the explicit env-var arms are covered: every other input reaches the + /// TTY check, and `cargo test -- --nocapture` from a terminal would then block + /// the suite on an interactive prompt. + #[test] + fn is_wanted_honours_an_explicit_env_var() { + temp_env::with_var(BUILD_FROM_SOURCE_ENV, Some("true"), || { + assert!(is_wanted()); + }); + temp_env::with_var(BUILD_FROM_SOURCE_ENV, Some("false"), || { + assert!(!is_wanted()); + }); + } +} diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 1e7fe32ca..a07b540fe 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -282,29 +282,31 @@ pub async fn install_valgrind( return Ok(()); } - // No package to publish means nothing to install automatically: try to build - // valgrind-codspeed from source instead. This is a best effort, the build toolchain may be - // missing or the build may fail, in which case the user is pointed to a manual installation. + // No package to publish means nothing to install automatically. Offer to build + // valgrind-codspeed from source instead: the build compiles for a few minutes and + // installs system-wide, so the user decides whether we do it or they install by hand. warn!( - "CodSpeed does not publish a valgrind package for {}, falling back to building it from source", + "CodSpeed does not publish a valgrind package for {}", system_info.os ); - let build_error = match build_from_source::build_and_install(system_info).await { - Ok(()) => { - if is_valgrind_installed() { + + if build_from_source::is_wanted() { + // A best effort: the toolchain may be missing or the build may fail, in which case + // the user is pointed to a manual installation like a declined build would be. + match build_from_source::build_and_install(system_info).await { + Ok(()) if is_valgrind_installed() => { info!("valgrind-codspeed has been built and installed from source"); warn_on_missing_libc_debug_symbols(system_info); return Ok(()); } - anyhow!("the freshly built valgrind is not usable, see the logs above") + Ok(()) => warn!("The freshly built valgrind is not usable, see the logs above"), + Err(error) => warn!("Building valgrind from source failed: {error}"), } - Err(error) => error, - }; + } bail!( - "CodSpeed does not publish a valgrind package for {}, and building it from source failed: {build_error}. \ - Install valgrind-codspeed {} or higher manually, see https://github.com/CodSpeedHQ/valgrind-codspeed", - system_info.os, + "valgrind-codspeed {} or higher is required and could not be installed automatically. \ + Install it manually, see https://github.com/CodSpeedHQ/valgrind-codspeed", VALGRIND_CODSPEED_VERSION_STRING.as_str() ); }