Skip to content
Open
56 changes: 33 additions & 23 deletions src/cli/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,39 +67,49 @@ async fn setup(modes: &[RunnerMode], setup_cache_dir: Option<&Path>) -> Result<(
}

/// Set up a single executor based on its support level on the current system.
///
/// Unsupported executors or executors that require manual installation are
/// skipped, not treated as fatal.
async fn setup_executor(
executor: &dyn Executor,
system_info: &SystemInfo,
setup_cache_dir: Option<&Path>,
) -> Result<()> {
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.",
let support_level = executor.support_level(system_info);
if support_level == ExecutorSupport::Unsupported {
info!(
"Skipping setup for the {} executor: not supported on {}",
executor.name(),
system_info.os
);
return Ok(());
}

info!(
"Setting up the environment for the executor: {}",
executor.name()
);
let result = setup_and_grant(executor, system_info, setup_cache_dir).await;

match result {
// We publish no tooling for this host, so the executor could only try. Leave the
// installation to the user and carry on with the other executors.
Err(error) if support_level == ExecutorSupport::RequiresManualInstallation => {
warn!(
"Could not set up the {} executor on {}, install its tooling manually: {error}",
executor.name(),
system_info.os
);
Ok(())
}
ExecutorSupport::FullySupported => {
info!(
"Setting up the environment for the executor: {}",
executor.name()
);
executor.setup(system_info, setup_cache_dir).await?;
executor.grant_privileges()?;
}
result => result,
}
Ok(())
}

async fn setup_and_grant(
executor: &dyn Executor,
system_info: &SystemInfo,
setup_cache_dir: Option<&Path>,
) -> Result<()> {
executor.setup(system_info, setup_cache_dir).await?;
executor.grant_privileges()
}

pub fn status(modes: &[RunnerMode]) -> Result<()> {
Expand Down
220 changes: 220 additions & 0 deletions src/executor/valgrind/build_from_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//! 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.
//!
//! 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 console::Term;
use std::env;
use std::ffi::OsStr;
use std::path::Path;
use std::process::{Command, Stdio};
use tempfile::TempDir;

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";

/// 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()
}

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 into a temporary directory, wiped once the build is done.
async fn clone_sources() -> Result<TempDir> {
let source_dir =
TempDir::new().context("failed to create a temporary directory for the sources")?;

let source_dir_str = source_dir.path().to_string_lossy().into_owned();
let mut builder = CommandBuilder::new("git");
builder.args([
"clone",
"--depth",
"1",
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<TempDir> {
let source_dir = clone_sources().await?;
let path = source_dir.path();

// 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(path, path.join("autogen.sh"), &[])).await?;
run_build_command(command_in(path, path.join("configure"), &[])).await?;
run_build_command(command_in(
path,
"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
}

/// 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();

// Default to yes on empty input (just pressing Enter), as the `[Y/n]` prompt announces.
let accepted =
answer.is_empty() || answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes");
Comment thread
moha-bekh marked this conversation as resolved.
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
/// to fall back to instructions for a manual installation.
pub(super) async fn build_and_install() -> Result<()> {
let missing_dependencies = missing_build_dependencies();
if !missing_dependencies.is_empty() {
bail!(
"the build toolchain is incomplete, install the missing tools: {}",
missing_dependencies.join(", ")
);
}

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.path()).await?;

Ok(())
}
19 changes: 17 additions & 2 deletions src/executor/valgrind/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ use crate::prelude::*;
use crate::system::{SupportedOs, SystemInfo};

use super::setup::get_valgrind_status;
use super::setup::install_valgrind;
use super::setup::install_valgrind_from_package;
use super::setup::is_codspeed_valgrind_installation_supported;
use super::setup::try_install_from_source;
use super::{helpers::perf_maps::harvest_perf_maps, helpers::venv_compat, measure};

pub struct ValgrindExecutor;
Expand Down Expand Up @@ -39,7 +40,21 @@ impl Executor for ValgrindExecutor {
}

async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> {
install_valgrind(system_info, setup_cache_dir).await?;
match self.support_level(system_info) {
ExecutorSupport::FullySupported => {
install_valgrind_from_package(system_info, setup_cache_dir).await?
}
// No package exists for this system, so there is nothing to install automatically.
ExecutorSupport::RequiresManualInstallation => {
try_install_from_source(system_info).await?
}
ExecutorSupport::Unsupported => {
bail!(
"The valgrind executor is not supported on {}",
system_info.os
)
}
}

if let Err(error) = venv_compat::symlink_libpython(None) {
warn!("Failed to symlink libpython");
Expand Down
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
Loading