use std::path::PathBuf;
use std::process::Command;
use crate::kernel::{encode_kernel_list, resolve_kernel_set};
pub(crate) const TEST_SUB_ARGV: &[&str] = &["nextest", "run"];
pub(crate) const COVERAGE_SUB_ARGV: &[&str] = &["llvm-cov", "nextest"];
pub(crate) const LLVM_COV_SUB_ARGV: &[&str] = &["llvm-cov"];
pub(crate) fn profraw_inject_for(
sub_argv: &[&str],
existing_env: Option<std::ffi::OsString>,
) -> Option<PathBuf> {
if sub_argv != TEST_SUB_ARGV || existing_env.is_some() {
return None;
}
let dir = ktstr::test_support::profraw_target_dir();
Some(dir.join("default-%p-%m.profraw"))
}
fn run_cargo_sub(
sub_argv: &[&str],
label: &str,
kernel: Vec<String>,
no_perf_mode: bool,
no_skip_mode: bool,
release: bool,
args: Vec<String>,
) -> Result<(), String> {
let mut cmd = Command::new("cargo");
cmd.args(sub_argv);
if release {
cmd.args(["--cargo-profile", "release"]);
}
cmd.args(&args);
if no_perf_mode {
cmd.env("KTSTR_NO_PERF_MODE", "1");
}
if no_skip_mode {
cmd.env("KTSTR_NO_SKIP_MODE", "1");
}
if let Some(pat) = profraw_inject_for(sub_argv, std::env::var_os("LLVM_PROFILE_FILE")) {
cmd.env("LLVM_PROFILE_FILE", pat);
}
if !kernel.is_empty() {
let resolved = resolve_kernel_set(&kernel)?;
if resolved.is_empty() {
return Err(
"--kernel: every supplied value parsed to empty / whitespace; \
omit the flag for auto-discovery, or supply a kernel \
identifier"
.to_string(),
);
}
let first_dir = &resolved[0].1;
tracing::debug!("cargo ktstr: using kernel {}", first_dir.display());
cmd.env(ktstr::KTSTR_KERNEL_ENV, first_dir);
if resolved.len() > 1 {
let encoded = encode_kernel_list(&resolved)?;
eprintln!(
"cargo ktstr: fanning gauntlet across {n} kernels",
n = resolved.len(),
);
cmd.env(ktstr::KTSTR_KERNEL_LIST_ENV, encoded);
}
}
precompute_cast_cache();
let target_dir_path = resolve_target_dir();
if let Some(anchor_path) = generate_btf_anchor(&target_dir_path, release) {
let existing = std::env::var("BPF_EXTRA_CFLAGS_PRE_INCL").unwrap_or_default();
let inject = format!("-include {} {existing}", anchor_path.display());
cmd.env("BPF_EXTRA_CFLAGS_PRE_INCL", inject.trim());
eprintln!("cargo ktstr: BTF type anchor at {}", anchor_path.display());
}
tracing::debug!("cargo ktstr: running {label}");
let status = cmd
.status()
.map_err(|e| format!("spawn cargo {}: {e}", sub_argv.join(" ")))?;
cleanup_shm();
if status.success() {
Ok(())
} else {
Err(format!(
"cargo {} exited with {}",
sub_argv.join(" "),
status
.code()
.map_or("signal".to_string(), |c| c.to_string()),
))
}
}
fn precompute_cast_cache() {
let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
let mut binaries = Vec::new();
for profile in ["debug", "release"] {
let dir = std::path::Path::new(&target_dir).join(profile);
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else {
continue;
};
if name_str.starts_with("scx_") && !name_str.contains('.') {
let path = entry.path();
if path.is_file() {
binaries.push(path);
}
}
}
}
if binaries.is_empty() {
return;
}
eprintln!(
"cargo ktstr: precomputing cast analysis for {} scheduler binaries",
binaries.len()
);
for binary in binaries {
let path = binary.clone();
std::thread::spawn(move || {
ktstr::precompute_cast_analysis(&path);
});
}
}
fn generate_btf_anchor(target_dir: &std::path::Path, release: bool) -> Option<std::path::PathBuf> {
let anchor_path = target_dir.join("ktstr_btf_anchor.h");
let profile = if release { "release" } else { "debug" };
let build_root = target_dir.join(profile).join("build");
let mut bpf_object_dirs: Vec<PathBuf> = Vec::new();
if let Ok(entries) = std::fs::read_dir(&build_root) {
for entry in entries.flatten() {
let out = entry.path().join("out");
if out.join("bpf.bpf.o").is_file() {
bpf_object_dirs.push(out);
}
}
}
if bpf_object_dirs.is_empty() {
return None;
}
bpf_object_dirs.sort_by_key(|d| {
std::cmp::Reverse(
std::fs::read_dir(d)
.map(|r| {
r.flatten()
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.ends_with(".bpf.o"))
})
.count()
})
.unwrap_or(0),
)
});
let bpf_object_dir = &bpf_object_dirs[0];
let mut cflags: Vec<String> = Vec::new();
if let Ok(base) = std::env::var("BPF_BASE_CFLAGS") {
cflags.extend(base.split_whitespace().map(String::from));
} else {
cflags.extend(["-g", "-O2"].iter().map(|s| s.to_string()));
}
if let Ok(pre) = std::env::var("BPF_EXTRA_CFLAGS_PRE_INCL") {
cflags.extend(pre.split_whitespace().map(String::from));
}
if let Ok(entries) = std::fs::read_dir(&build_root) {
for entry in entries.flatten() {
let bpf_h = entry.path().join("out/scx_utils-bpf_h");
if bpf_h.is_dir() {
cflags.push(format!("-I{}", bpf_h.display()));
}
}
}
if let Ok(post) = std::env::var("BPF_EXTRA_CFLAGS_POST_INCL") {
cflags.extend(post.split_whitespace().map(String::from));
}
let clang = std::env::var("BPF_CLANG").unwrap_or_else(|_| "clang".to_string());
crate::btf_catalog::generate_btf_anchor(bpf_object_dir, &clang, &cflags, &anchor_path)
}
fn resolve_target_dir() -> std::path::PathBuf {
if let Ok(d) = std::env::var("CARGO_TARGET_DIR") {
return std::path::PathBuf::from(d);
}
if let Ok(output) = Command::new("cargo")
.args(["metadata", "--format-version=1", "--no-deps"])
.output()
&& output.status.success()
&& let Ok(v) = serde_json::from_slice::<serde_json::Value>(&output.stdout)
&& let Some(dir) = v["target_directory"].as_str()
{
return std::path::PathBuf::from(dir);
}
std::path::PathBuf::from("target")
}
fn cleanup_shm() {
let Ok(dir) = std::fs::read_dir("/dev/shm") else {
return;
};
for entry in dir.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else {
continue;
};
if !name_str.starts_with("ktstr-base-")
&& !name_str.starts_with("ktstr-lz4-")
&& !name_str.starts_with("ktstr-gz-")
{
continue;
}
let shm_name = format!("/{name_str}");
let Ok(fd) = rustix::shm::open(
shm_name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
) else {
continue;
};
if rustix::fs::flock(&fd, rustix::fs::FlockOperation::NonBlockingLockExclusive).is_err() {
continue;
}
let _ = rustix::shm::unlink(shm_name.as_str());
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
}
}
pub(crate) fn run_test(
kernel: Vec<String>,
no_perf_mode: bool,
no_skip_mode: bool,
release: bool,
args: Vec<String>,
) -> Result<(), String> {
ktstr::cli::check_kvm().map_err(|e| format!("{e:#}"))?;
ktstr::cli::check_tools(&["cargo-nextest"]).map_err(|e| format!("{e:#}"))?;
run_cargo_sub(
TEST_SUB_ARGV,
"tests",
kernel,
no_perf_mode,
no_skip_mode,
release,
args,
)
}
pub(crate) fn run_coverage(
kernel: Vec<String>,
no_perf_mode: bool,
no_skip_mode: bool,
release: bool,
args: Vec<String>,
) -> Result<(), String> {
ktstr::cli::check_kvm().map_err(|e| format!("{e:#}"))?;
ktstr::cli::check_tools(&["cargo-nextest", "cargo-llvm-cov"]).map_err(|e| format!("{e:#}"))?;
run_cargo_sub(
COVERAGE_SUB_ARGV,
"coverage",
kernel,
no_perf_mode,
no_skip_mode,
release,
args,
)
}
pub(crate) fn run_llvm_cov(
kernel: Vec<String>,
no_perf_mode: bool,
no_skip_mode: bool,
args: Vec<String>,
) -> Result<(), String> {
run_cargo_sub(
LLVM_COV_SUB_ARGV,
"llvm-cov",
kernel,
no_perf_mode,
no_skip_mode,
false,
args,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cargo_sub_argv_constants_are_pinned() {
assert_eq!(TEST_SUB_ARGV, &["nextest", "run"]);
assert_eq!(COVERAGE_SUB_ARGV, &["llvm-cov", "nextest"]);
assert_eq!(LLVM_COV_SUB_ARGV, &["llvm-cov"]);
}
#[test]
fn profraw_inject_for_test_path_returns_pattern() {
let pat = profraw_inject_for(TEST_SUB_ARGV, None)
.expect("test path without LLVM_PROFILE_FILE must inject");
assert!(
pat.ends_with("default-%p-%m.profraw"),
"injected pattern must end with default-%%p-%%m.profraw, got {}",
pat.display(),
);
assert_ne!(
pat.as_os_str(),
"default-%p-%m.profraw",
"pattern must be absolute (carry a target dir prefix), \
not bare so the LLVM runtime never falls back to cwd",
);
}
#[test]
fn profraw_inject_for_coverage_path_skips() {
assert!(
profraw_inject_for(COVERAGE_SUB_ARGV, None).is_none(),
"coverage path must not inject — cargo-llvm-cov owns LLVM_PROFILE_FILE",
);
}
#[test]
fn profraw_inject_for_llvm_cov_path_skips() {
assert!(
profraw_inject_for(LLVM_COV_SUB_ARGV, None).is_none(),
"llvm-cov passthrough path must not inject — user owns env decisions",
);
}
#[test]
fn profraw_inject_for_respects_operator_override() {
let existing = std::ffi::OsString::from("/tmp/operator-pinned-%p.profraw");
assert!(
profraw_inject_for(TEST_SUB_ARGV, Some(existing)).is_none(),
"an operator-set LLVM_PROFILE_FILE must not be overridden",
);
}
}