Skip to content

Commit 6c62c72

Browse files
feat: auto install codspeed-memtrack during executor setup
1 parent dfa91c6 commit 6c62c72

4 files changed

Lines changed: 243 additions & 13 deletions

File tree

src/binary_installer/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use crate::prelude::*;
2+
use crate::run::helpers::download_file;
3+
use semver::Version;
4+
use std::process::Command;
5+
use tempfile::NamedTempFile;
6+
use url::Url;
7+
8+
mod versions;
9+
10+
/// Ensure a binary is installed, or install it from a runner's GitHub release using the installer script.
11+
///
12+
/// This function checks if the binary is already installed with the correct version.
13+
/// If not, it downloads and executes an installer script from the CodSpeed runner repository.
14+
///
15+
/// # Arguments
16+
/// * `binary_name` - The binary command name (e.g., "codspeed-memtrack", "codspeed-exec-harness")
17+
/// * `version` - The version to install (e.g., "4.4.2-alpha.2")
18+
/// * `get_installer_url` - A closure that returns the URL to download the installer script.
19+
pub async fn ensure_binary_installed<F>(
20+
binary_name: &str,
21+
version: &str,
22+
get_installer_url: F,
23+
) -> Result<()>
24+
where
25+
F: FnOnce() -> String,
26+
{
27+
if is_command_installed(
28+
binary_name,
29+
Version::parse(version).context("Invalid version format")?,
30+
) {
31+
debug!("{binary_name} version {version} is already installed");
32+
return Ok(());
33+
}
34+
35+
let installer_url = Url::parse(&get_installer_url()).context("Invalid installer URL")?;
36+
37+
debug!("Downloading installer from: {installer_url}");
38+
39+
// Download the installer script to a temporary file
40+
let temp_file = NamedTempFile::new().context("Failed to create temporary file")?;
41+
download_file(&installer_url, temp_file.path()).await?;
42+
43+
// Execute the installer script
44+
let output = Command::new("sh")
45+
.arg(temp_file.path())
46+
.output()
47+
.context("Failed to execute installer command")?;
48+
49+
if !output.status.success() {
50+
bail!(
51+
"Failed to install {binary_name} version {version}. Installer exited with output: {output:?}",
52+
);
53+
}
54+
55+
info!("Successfully installed {binary_name} version {version}");
56+
Ok(())
57+
}
58+
59+
/// Check if the given command is installed and its version matches the expected version.
60+
///
61+
/// Expects the command to support the `--version` flag and return a version string.
62+
fn is_command_installed(command: &str, expected_version: Version) -> bool {
63+
let is_command_installed = Command::new("which")
64+
.arg(command)
65+
.output()
66+
.is_ok_and(|output| output.status.success());
67+
68+
if !is_command_installed {
69+
debug!("{command} is not installed");
70+
return false;
71+
}
72+
73+
let Ok(version_output) = Command::new(command).arg("--version").output() else {
74+
return false;
75+
};
76+
77+
if !version_output.status.success() {
78+
debug!(
79+
"Failed to get valgrind version. stderr: {}",
80+
String::from_utf8_lossy(&version_output.stderr)
81+
);
82+
return false;
83+
}
84+
85+
let version_string = String::from_utf8_lossy(&version_output.stdout);
86+
let Ok(version) = versions::parse_from_output(&version_string) else {
87+
return false;
88+
};
89+
90+
debug!("Found {command} version: {version}");
91+
92+
versions::is_compatible(command, &version, &expected_version)
93+
}

src/binary_installer/versions.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use crate::prelude::*;
2+
use semver::Version;
3+
4+
/// Parse a version string from command output.
5+
///
6+
/// Expects the output format to be: "command_name version_string"
7+
/// Example: "codspeed-memtrack 4.4.2"
8+
pub(super) fn parse_from_output(output: &str) -> Result<Version> {
9+
let version_str = output
10+
.split_once(" ")
11+
.context("Unexpected version output format: missing space separator")?
12+
.1
13+
.trim();
14+
15+
Version::parse(version_str)
16+
.with_context(|| format!("Failed to parse version from: {version_str}"))
17+
}
18+
19+
/// Check if an installed version is compatible with the expected version.
20+
///
21+
/// Returns true if the installed version is greater than or equal to the expected version.
22+
/// Logs warnings for outdated or experimental versions.
23+
pub(super) fn is_compatible(command: &str, installed: &Version, expected: &Version) -> bool {
24+
match installed.cmp(expected) {
25+
std::cmp::Ordering::Less => {
26+
warn!(
27+
"{command} is installed but the version is too old. expecting {expected} or higher but found installed: {installed}",
28+
);
29+
false
30+
}
31+
std::cmp::Ordering::Greater => {
32+
warn!(
33+
"Using experimental {command} version {installed}. The recommended version is {expected}",
34+
);
35+
true
36+
}
37+
std::cmp::Ordering::Equal => true,
38+
}
39+
}
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
44+
mod parse_version_from_output {
45+
use super::*;
46+
47+
#[test]
48+
fn parses_valid_version() {
49+
let output = "codspeed-memtrack 4.4.2";
50+
let version = parse_from_output(output).unwrap();
51+
assert_eq!(version, Version::new(4, 4, 2));
52+
}
53+
54+
#[test]
55+
fn parses_version_with_prerelease() {
56+
let output = "codspeed-exec-harness 4.4.2-alpha.2";
57+
let version = parse_from_output(output).unwrap();
58+
assert_eq!(version.major, 4);
59+
assert_eq!(version.minor, 4);
60+
assert_eq!(version.patch, 2);
61+
assert_eq!(version.pre.as_str(), "alpha.2");
62+
}
63+
}
64+
65+
mod is_version_compatible {
66+
use super::*;
67+
68+
#[test]
69+
fn returns_true_for_equal_versions() {
70+
let installed = Version::new(4, 4, 2);
71+
let expected = Version::new(4, 4, 2);
72+
assert!(is_compatible("test-cmd", &installed, &expected));
73+
}
74+
75+
#[test]
76+
fn returns_true_for_newer_version() {
77+
let installed = Version::new(4, 5, 0);
78+
let expected = Version::new(4, 4, 2);
79+
assert!(is_compatible("test-cmd", &installed, &expected));
80+
}
81+
82+
#[test]
83+
fn returns_false_for_older_version() {
84+
let installed = Version::new(4, 3, 0);
85+
let expected = Version::new(4, 4, 2);
86+
assert!(!is_compatible("test-cmd", &installed, &expected));
87+
}
88+
89+
#[test]
90+
fn handles_prerelease_versions() {
91+
let installed = Version::parse("4.4.2-alpha.2").unwrap();
92+
let expected = Version::new(4, 4, 1);
93+
// 4.4.2-alpha.2 > 4.4.1 because 4.4.2 > 4.4.1
94+
assert!(is_compatible("test-cmd", &installed, &expected));
95+
}
96+
97+
#[test]
98+
fn prerelease_different_stage() {
99+
{
100+
let installed = Version::parse("4.4.2-alpha.2").unwrap();
101+
let expected = Version::new(4, 4, 2);
102+
// 4.4.2-alpha.2 < 4.4.2
103+
assert!(!is_compatible("test-cmd", &installed, &expected));
104+
}
105+
106+
{
107+
let installed = Version::parse("4.4.2-beta.1").unwrap();
108+
let expected = Version::parse("4.4.2-alpha.1").unwrap();
109+
assert!(is_compatible("test-cmd", &installed, &expected));
110+
}
111+
112+
{
113+
let installed = Version::new(4, 4, 2);
114+
let expected = Version::parse("4.4.2-alpha.2").unwrap();
115+
// 4.4.2 > 4.4.2-alpha.2
116+
assert!(is_compatible("test-cmd", &installed, &expected));
117+
}
118+
119+
{
120+
let installed = Version::parse("4.4.2-alpha.1").unwrap();
121+
let expected = Version::parse("4.4.2-beta.1").unwrap();
122+
assert!(!is_compatible("test-cmd", &installed, &expected));
123+
}
124+
}
125+
126+
#[test]
127+
fn prerelease_same_stage() {
128+
let installed = Version::parse("4.4.2-alpha.1").unwrap();
129+
let expected = Version::parse("4.4.2-alpha.2").unwrap();
130+
131+
assert!(!is_compatible("test-cmd", &installed, &expected));
132+
}
133+
}
134+
}

src/executor/memory/executor.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::binary_installer::ensure_binary_installed;
12
use crate::executor::ExecutorName;
23
use crate::executor::helpers::command::CommandBuilder;
34
use crate::executor::helpers::get_bench_command::get_bench_command;
@@ -16,9 +17,11 @@ use runner_shared::artifacts::{ArtifactExt, ExecutionTimestamps};
1617
use runner_shared::fifo::Command as FifoCommand;
1718
use runner_shared::fifo::IntegrationMode;
1819
use std::path::Path;
19-
use std::process::Command;
2020
use std::rc::Rc;
2121

22+
const MEMTRACK_COMMAND: &str = "codspeed-memtrack";
23+
const MEMTRACK_CODSPEED_VERSION: &str = "1.0.0";
24+
2225
pub struct MemoryExecutor;
2326

2427
impl MemoryExecutor {
@@ -57,19 +60,18 @@ impl Executor for MemoryExecutor {
5760
_system_info: &SystemInfo,
5861
_setup_cache_dir: Option<&Path>,
5962
) -> Result<()> {
60-
// Validate that the codspeed-memtrack command is available
61-
let memtrack_path = std::env::var("CODSPEED_MEMTRACK_BINARY")
62-
.unwrap_or_else(|_| "codspeed-memtrack".to_string());
63+
let get_memtrack_installer_url = || {
64+
format!(
65+
"https://github.com/CodSpeedHQ/runner/releases/download/memtrack-v{MEMTRACK_CODSPEED_VERSION}/memtrack-installer.sh"
66+
)
67+
};
6368

64-
info!("Validating memtrack binary at path: {memtrack_path}");
65-
let output = Command::new(&memtrack_path).arg("--version").output()?;
66-
if !output.status.success() {
67-
bail!(
68-
"codspeed-memtrack command is not available or failed to execute\nstdout: {}\nstderr: {}",
69-
String::from_utf8_lossy(&output.stdout),
70-
String::from_utf8_lossy(&output.stderr)
71-
);
72-
}
69+
ensure_binary_installed(
70+
MEMTRACK_COMMAND,
71+
MEMTRACK_CODSPEED_VERSION,
72+
get_memtrack_installer_url,
73+
)
74+
.await?;
7375

7476
Ok(())
7577
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod api_client;
22
mod app;
33
mod auth;
4+
mod binary_installer;
45
mod config;
56
mod exec;
67
mod executor;

0 commit comments

Comments
 (0)