Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions src/executor/valgrind/build_from_source.rs
Original file line number Diff line number Diff line change
@@ -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<S: AsRef<OsStr>>(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<PathBuf> {
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<PathBuf> {
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(())
}
1 change: 1 addition & 0 deletions src/executor/valgrind/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod build_from_source;
pub mod executor;
pub mod helpers;
mod measure;
Expand Down
39 changes: 39 additions & 0 deletions src/executor/valgrind/setup.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -245,6 +246,44 @@ 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(());
}

// 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 {}, 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()
Comment on lines +279 to +283

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Manual requirement omitted

On unsupported Ubuntu or Debian versions, is_valgrind_installed also requires resolvable libc debug symbols, but this error only instructs users to install valgrind-codspeed. A user who follows that instruction can receive the same error again without learning that the debug-symbol package is the remaining requirement.

Knowledge Base Used: Valgrind measurement

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/executor/valgrind/setup.rs
Line: 260-264

Comment:
**Manual requirement omitted**

On unsupported Ubuntu or Debian versions, `is_valgrind_installed` also requires resolvable libc debug symbols, but this error only instructs users to install `valgrind-codspeed`. A user who follows that instruction can receive the same error again without learning that the debug-symbol package is the remaining requirement.

**Knowledge Base Used:** [Valgrind measurement](https://app.greptile.com/codspeed/-/custom-context/knowledge-base/codspeedhq/codspeed/-/docs/valgrind-measurement.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

);
}

apt::install_cached(
system_info,
setup_cache_dir,
Expand Down
24 changes: 22 additions & 2 deletions src/system/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Self> {
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}"),
}
Expand Down Expand Up @@ -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());
}
}