use std::env;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use libbpf_cargo::SkeletonBuilder;
include!("src/kernel_path.rs");
include!("src/build_helpers.rs");
fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
println!("cargo:rerun-if-env-changed=KTSTR_KERNEL");
println!("cargo:rerun-if-changed=src/kernel_path.rs");
println!("cargo:rerun-if-changed=src/bpf/vmlinux_gen.c");
let ktstr_kernel = env::var("KTSTR_KERNEL").ok();
let vmlinux_h = out_dir.join("vmlinux.h");
let hash_path = out_dir.join("vmlinux.btf.hash");
let current_btf = resolve_btf(ktstr_kernel.as_deref());
let current_hash: Option<String> = current_btf.as_ref().and_then(|p| match std::fs::read(p) {
Ok(bytes) => Some(format!("{:016x}", siphash_13(&bytes))),
Err(e) => {
println!(
"cargo:warning=BTF source {} present but unreadable \
({e}); skipping hash check, reusing existing vmlinux.h",
p.display(),
);
None
}
});
let stored_hash: Option<String> = std::fs::read_to_string(&hash_path)
.ok()
.map(|s| s.trim().to_string());
let should_regen =
!vmlinux_h.exists() || (current_hash.is_some() && current_hash != stored_hash);
if should_regen {
let btf_source = current_btf.unwrap_or_else(|| {
panic!(
"no BTF source found. Set KTSTR_KERNEL to a kernel build \
directory, or ensure /sys/kernel/btf/vmlinux exists."
);
});
println!("generating vmlinux.h from {}", btf_source.display());
let libbpf_include =
PathBuf::from(env::var("DEP_BPF_INCLUDE").expect("DEP_BPF_INCLUDE not set"));
let vmlinux_gen_bin = out_dir.join("vmlinux_gen");
let driver_src = out_dir.join("vmlinux_gen_main.c");
std::fs::write(
&driver_src,
format!(
r#"
extern int generate_vmlinux_h(const char *, const char *);
int main(void) {{
return generate_vmlinux_h("{btf}", "{out}") == 0 ? 0 : 1;
}}
"#,
btf = btf_source.display(),
out = vmlinux_h.display(),
),
)
.expect("write driver source");
let libbpf_lib_dir = libbpf_include.parent().unwrap();
let compiler = cc::Build::new().get_compiler();
let status = Command::new(compiler.path())
.args([
"src/bpf/vmlinux_gen.c",
driver_src.to_str().unwrap(),
"-o",
vmlinux_gen_bin.to_str().unwrap(),
&format!("-I{}", libbpf_include.display()),
&format!("-L{}", libbpf_lib_dir.display()),
"-lbpf",
"-lelf",
"-lz",
])
.status()
.expect("compile vmlinux_gen");
assert!(status.success(), "failed to compile vmlinux_gen");
let status = Command::new(&vmlinux_gen_bin)
.status()
.expect("run vmlinux_gen");
assert!(
status.success(),
"vmlinux_gen failed — check BTF source: {}",
btf_source.display()
);
let hash_opt: Option<String> = match current_hash.as_deref() {
Some(h) => Some(h.to_string()),
None => match std::fs::read(&btf_source) {
Ok(bytes) => Some(format!("{:016x}", siphash_13(&bytes))),
Err(e) => {
println!(
"cargo:warning=post-regen BTF re-read failed ({e}); \
skipping hash sidecar — next build.rs run will \
regenerate conservatively"
);
None
}
},
};
if let Some(hash) = hash_opt {
std::fs::write(&hash_path, format!("{hash}\n"))
.unwrap_or_else(|e| panic!("write BTF hash sidecar {}: {e}", hash_path.display()));
}
}
if cfg!(target_arch = "aarch64") {
let content = std::fs::read_to_string(&vmlinux_h).expect("read vmlinux.h");
if !content.contains("struct user_pt_regs {") {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&vmlinux_h)
.expect("open vmlinux.h for append");
writeln!(
f,
"\n/* Added by build.rs: arm64 UAPI type needed by bpf_tracing.h */\n\
struct user_pt_regs {{\n\
\t__u64 regs[31];\n\
\t__u64 sp;\n\
\t__u64 pc;\n\
\t__u64 pstate;\n\
}};\n"
)
.expect("append user_pt_regs to vmlinux.h");
}
}
let clang_args = [
format!("-I{}", out_dir.display()),
format!("-I{}", "src/bpf"),
];
let skel_path = out_dir.join("probe_skel.rs");
SkeletonBuilder::new()
.source("src/bpf/probe.bpf.c")
.obj(out_dir.join("probe.o"))
.clang_args(clang_args.clone())
.reference_obj(true)
.build_and_generate(&skel_path)
.expect("build probe BPF skeleton");
let fentry_skel_path = out_dir.join("fentry_probe_skel.rs");
SkeletonBuilder::new()
.source("src/bpf/fentry_probe.bpf.c")
.obj(out_dir.join("fentry_probe.o"))
.clang_args(clang_args)
.reference_obj(true)
.build_and_generate(&fentry_skel_path)
.expect("build fentry probe BPF skeleton");
println!("cargo::rerun-if-changed=src/bpf/probe.bpf.c");
println!("cargo::rerun-if-changed=src/bpf/fentry_probe.bpf.c");
println!("cargo::rerun-if-changed=src/bpf/intf.h");
generate_shift_registry(&out_dir);
println!(
"cargo:rustc-env=KTSTR_CAST_ANALYZER_FINGERPRINT={:016x}",
cast_analyzer_fingerprint()
);
println!(
"cargo:rustc-env=KTSTR_CARGO_LOCK_FINGERPRINT={:016x}",
cargo_lock_fingerprint()
);
let busybox_bin = out_dir.join("busybox");
println!("cargo:rerun-if-env-changed=KTSTR_SKIP_BUSYBOX_BUILD");
println!("cargo:rerun-if-env-changed=KTSTR_BUSYBOX_TARBALL");
let skip_busybox = std::env::var("KTSTR_SKIP_BUSYBOX_BUILD")
.ok()
.filter(|v| !v.is_empty())
.is_some();
if skip_busybox {
println!(
"cargo:warning=KTSTR_SKIP_BUSYBOX_BUILD set — writing 0-byte \
$OUT_DIR/busybox placeholder; shell mode will be unavailable \
in the resulting cargo-ktstr binary"
);
if !busybox_bin.exists() {
std::fs::write(&busybox_bin, b"").unwrap_or_else(|e| {
panic!(
"write 0-byte busybox placeholder {}: {e}",
busybox_bin.display()
)
});
}
} else if !busybox_bin.exists() {
println!("cargo:warning=compiling busybox (first build only)...");
if Command::new("make").arg("--version").output().is_err() {
panic!(
"busybox build requires 'make' — install build-essential \
(Debian/Ubuntu) or base-devel (Fedora/Arch)"
);
}
if Command::new("gcc").arg("--version").output().is_err() {
panic!(
"busybox build requires 'gcc' — install build-essential \
(Debian/Ubuntu) or base-devel (Fedora/Arch)"
);
}
let busybox_src = out_dir.join("busybox-src");
if busybox_src.exists() && !busybox_src.join("Makefile").exists() {
std::fs::remove_dir_all(&busybox_src).expect("remove incomplete busybox-src");
}
if !busybox_src.join("Makefile").exists() {
const TARBALL_URL: &str =
"https://github.com/mirror/busybox/archive/refs/tags/1_36_1.tar.gz";
let tarball_bytes = match std::env::var("KTSTR_BUSYBOX_TARBALL")
.ok()
.filter(|v| !v.is_empty())
{
Some(local) => {
println!(
"cargo:warning=KTSTR_BUSYBOX_TARBALL set — reading {local} \
instead of fetching from {TARBALL_URL}"
);
std::fs::read(&local).unwrap_or_else(|e| {
panic!(
"read KTSTR_BUSYBOX_TARBALL={local}: {e} — the env \
var must point at a readable tarball matching the \
pinned SHA-256"
)
})
}
None => fetch_busybox_tarball(TARBALL_URL),
};
verify_busybox_tarball_sha256(&tarball_bytes);
let extract_dir = out_dir.join("busybox-extract");
if extract_dir.exists() {
let _ = std::fs::remove_dir_all(&extract_dir);
}
let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(&tarball_bytes[..]));
let mut archive = tar::Archive::new(gz);
archive
.unpack(&extract_dir)
.unwrap_or_else(|e| panic!("extract busybox tarball: {e}"));
let inner = extract_dir.join("busybox-1_36_1");
std::fs::rename(&inner, &busybox_src).unwrap_or_else(|e| {
panic!(
"expected extracted directory {} — tarball layout may have changed: {e}",
inner.display()
)
});
std::fs::remove_dir_all(&extract_dir).ok();
}
let status = Command::new("make")
.arg("defconfig")
.current_dir(&busybox_src)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("make defconfig");
assert!(status.success(), "busybox make defconfig failed");
let config_path = busybox_src.join(".config");
let config = std::fs::read_to_string(&config_path).expect("read busybox .config");
let config = config
.replace("# CONFIG_STATIC is not set", "CONFIG_STATIC=y")
.replace("CONFIG_TC=y", "# CONFIG_TC is not set");
std::fs::write(&config_path, config).expect("write patched busybox .config");
let status = Command::new("make")
.arg("oldconfig")
.current_dir(&busybox_src)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("make oldconfig");
assert!(status.success(), "busybox make oldconfig failed");
let status = Command::new("make")
.arg("-j1")
.current_dir(&busybox_src)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("busybox make");
assert!(status.success(), "busybox build failed");
std::fs::copy(busybox_src.join("busybox"), &busybox_bin)
.expect("copy busybox binary to OUT_DIR");
}
let wprof_bin = out_dir.join("wprof");
#[cfg(not(feature = "wprof"))]
if !wprof_bin.exists() {
std::fs::write(&wprof_bin, b"").unwrap_or_else(|e| {
panic!(
"write 0-byte wprof placeholder {}: {e}",
wprof_bin.display()
)
});
}
#[cfg(feature = "wprof")]
{
println!("cargo:rerun-if-env-changed=KTSTR_SKIP_WPROF_BUILD");
let skip_wprof = std::env::var("KTSTR_SKIP_WPROF_BUILD")
.ok()
.filter(|v| !v.is_empty())
.is_some();
if skip_wprof {
println!(
"cargo:warning=KTSTR_SKIP_WPROF_BUILD set — writing 0-byte \
$OUT_DIR/wprof placeholder; do NOT use the resulting \
cargo-ktstr binary for wprof capture"
);
if !wprof_bin.exists() {
std::fs::write(&wprof_bin, b"").unwrap_or_else(|e| {
panic!(
"write 0-byte wprof placeholder {}: {e}",
wprof_bin.display()
)
});
}
} else if !wprof_bin.exists() {
println!("cargo:warning=cloning + compiling wprof (first build only)...");
for tool in ["git", "make", "gcc", "clang"] {
if Command::new(tool).arg("--version").output().is_err() {
panic!(
"wprof build requires '{tool}' on PATH — install via your \
distro's package manager (build-essential / base-devel for \
make+gcc; clang for BPF skeleton compile; git for \
submodule clone)"
);
}
}
let wprof_src = out_dir.join("wprof-src");
let wprof_makefile = wprof_src.join("src").join("Makefile");
if wprof_src.exists() && !is_wprof_clone_complete(&wprof_src) {
std::fs::remove_dir_all(&wprof_src).expect("remove incomplete wprof-src");
}
if !wprof_makefile.exists() {
let git_url = "https://github.com/anakryiko/wprof.git";
println!(
"cargo:warning=cloning {git_url} into {} (recursive — \
pulls libbpf, bpftool, blazesym, vmlinux.h, usdt, \
strobelight-libs)",
wprof_src.display()
);
const MAX_CLONE_ATTEMPTS: u32 = 4;
let clone_attempt = |i: u32| -> Result<(), String> {
if i > 1
&& let Err(e) = std::fs::remove_dir_all(&wprof_src)
{
println!(
"cargo:warning=wprof partial-clone cleanup before attempt {i} \
failed: {e}; continuing to next attempt anyway"
);
}
let status = Command::new("git")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "/bin/false")
.arg("-c")
.arg("http.lowSpeedLimit=1000")
.arg("-c")
.arg("http.lowSpeedTime=60")
.arg("clone")
.arg("--recurse-submodules")
.arg("--depth=1")
.arg("--shallow-submodules")
.arg(git_url)
.arg(&wprof_src)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("spawn git clone for wprof");
if status.success() {
Ok(())
} else {
Err(format!("git clone exited {status}"))
}
};
if let Err(err) =
retry_with_backoff("wprof git clone", MAX_CLONE_ATTEMPTS, clone_attempt)
{
panic!(
"wprof git clone failed after {MAX_CLONE_ATTEMPTS} attempts \
(last error: {err}). Check network connectivity to \
{git_url}; if the cache directory is in an \
unrecoverable state, `rm -rf {}` and re-run `cargo build`.",
wprof_src.display()
);
}
}
let demangle_manifest = wprof_src.join("src").join("demangle").join("Cargo.toml");
if demangle_manifest.exists() {
let existing = std::fs::read_to_string(&demangle_manifest)
.unwrap_or_else(|e| panic!("read {}: {e}", demangle_manifest.display()));
let already_patched = existing.lines().any(|l| l.trim() == "[workspace]");
if !already_patched {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&demangle_manifest)
.unwrap_or_else(|e| {
panic!("open {} for append: {e}", demangle_manifest.display())
});
f.write_all(b"\n[workspace]\n").unwrap_or_else(|e| {
panic!("append [workspace] to {}: {e}", demangle_manifest.display())
});
}
}
let status = Command::new("make")
.arg("-j1")
.current_dir(wprof_src.join("src"))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("spawn make for wprof");
assert!(status.success(), "wprof build failed");
let built_bin = wprof_src.join("src").join("wprof");
assert!(
built_bin.exists(),
"wprof build succeeded but binary not found at expected path: {}",
built_bin.display()
);
std::fs::copy(&built_bin, &wprof_bin).expect("copy wprof binary to OUT_DIR");
}
} }
const BUSYBOX_TARBALL_SHA256: &str = "";
fn fetch_busybox_tarball(url: &str) -> Vec<u8> {
let github_token = std::env::var("GITHUB_TOKEN").ok();
let attempt = |attempt_idx: u32| -> Result<Vec<u8>, String> {
let mut client_builder = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.connect_timeout(std::time::Duration::from_secs(30))
.user_agent(concat!("ktstr-build/", env!("CARGO_PKG_VERSION")));
if let Ok(proxy_url) = std::env::var("HTTPS_PROXY")
.or_else(|_| std::env::var("https_proxy"))
.or_else(|_| std::env::var("HTTP_PROXY"))
.or_else(|_| std::env::var("http_proxy"))
{
let proxy = reqwest::Proxy::all(&proxy_url)
.map_err(|e| format!("invalid proxy URL {proxy_url}: {e}"))?;
client_builder = client_builder.proxy(proxy);
}
let client = client_builder
.build()
.map_err(|e| format!("http client: {e}"))?;
let mut req = client.get(url);
if let Some(ref token) = github_token {
req = req.bearer_auth(token);
}
let resp = req
.send()
.and_then(|r| r.error_for_status())
.map_err(|e| format!("attempt {attempt_idx} request: {e}"))?;
let body = resp
.bytes()
.map_err(|e| format!("attempt {attempt_idx} body: {e}"))?;
Ok(body.to_vec())
};
println!("cargo:warning=downloading busybox source tarball from {url}");
const MAX_TARBALL_ATTEMPTS: u32 = 4;
retry_with_backoff("busybox tarball download", MAX_TARBALL_ATTEMPTS, attempt).unwrap_or_else(
|e| {
panic!(
"failed to obtain busybox source after {MAX_TARBALL_ATTEMPTS} attempts.\n\
tarball ({url}): {e}\n\
Remediation:\n\
• Check network connectivity (the build script needs HTTPS\n\
access to github.com to fetch the upstream tarball).\n\
• If behind a proxy, ensure HTTP_PROXY/HTTPS_PROXY environment\n\
variables are set (e.g., export HTTPS_PROXY=http://proxy:8080).\n\
• Or set KTSTR_BUSYBOX_TARBALL=<path> to point at a\n\
pre-fetched local copy of {url} — useful for air-gapped\n\
CI runners and hermetic build environments.\n\
• Or set KTSTR_SKIP_BUSYBOX_BUILD=1 to skip the busybox\n\
compile entirely (shell mode will be unavailable in the\n\
resulting cargo-ktstr binary).",
)
},
)
}
fn verify_busybox_tarball_sha256(tarball_bytes: &[u8]) {
use sha2::{Digest, Sha256};
let actual = {
let mut hasher = Sha256::new();
hasher.update(tarball_bytes);
hex_encode_lowercase(&hasher.finalize())
};
if BUSYBOX_TARBALL_SHA256.is_empty() {
println!(
"cargo:warning=BUSYBOX_TARBALL_SHA256 is unset — first-build \
bootstrap. Computed SHA-256: {actual}\n\
To lock the pin: update BUSYBOX_TARBALL_SHA256 in build.rs to\n\
this value and commit. Subsequent builds will fail on mismatch."
);
return;
}
if !BUSYBOX_TARBALL_SHA256.eq_ignore_ascii_case(&actual) {
panic!(
"busybox tarball SHA-256 mismatch.\n\
expected: {BUSYBOX_TARBALL_SHA256}\n\
actual: {actual}\n\
\n\
Diagnose:\n\
• If the upstream archive was regenerated (rare — github\n\
changed archive generation in early 2023, otherwise these\n\
tarballs are stable for years), update BUSYBOX_TARBALL_SHA256\n\
in build.rs to the new digest after independently verifying\n\
the source.\n\
• Otherwise treat as a supply-chain alert: compare against\n\
the upstream SHA published by the busybox maintainers\n\
before continuing."
);
}
}
fn hex_encode_lowercase(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
write!(&mut s, "{b:02x}").expect("write to String never fails");
}
s
}
fn generate_shift_registry(out_dir: &std::path::Path) {
use std::fmt::Write;
println!("cargo::rerun-if-changed=src/budget.rs");
let budget_rs = std::fs::read_to_string("src/budget.rs")
.expect("read src/budget.rs for shift-registry scan");
let mut shifts: Vec<(u32, String)> = Vec::new();
for line in budget_rs.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("const ") else {
continue;
};
let Some((name_part, val_part)) = rest.split_once(": u32 = ") else {
continue;
};
let name = name_part.trim();
if !name.ends_with("_SHIFT") {
continue;
}
let val_str = val_part.trim_end_matches(';').trim();
let val: u32 = val_str.parse().unwrap_or_else(|e| {
panic!("shift-registry scan: parse `{val_str}` as u32 for {name}: {e}")
});
shifts.push((val, name.to_string()));
}
shifts.sort_by_key(|(v, _)| *v);
let mut out = String::from(
"// Generated by build.rs. Lists every `const *_SHIFT: u32 = N;`\n\
// declaration in src/budget.rs, sorted by shift value. The\n\
// budget tests assert their hand-classified one-bit and\n\
// multi-bit enumerations cover every entry so a new SHIFT\n\
// cannot land without being classified into the right test.\n\
pub(crate) const ALL_SHIFTS: &[(u32, &str)] = &[\n",
);
for (v, name) in &shifts {
writeln!(out, " ({v}, \"{name}\"),").expect("write shift entry");
}
out.push_str("];\n");
std::fs::write(out_dir.join("shift_registry.rs"), out).expect("write shift_registry.rs");
}
fn siphash_13(bytes: &[u8]) -> u64 {
use siphasher::sip::SipHasher13;
use std::hash::Hasher;
let mut h = SipHasher13::new_with_keys(0, 0);
h.write(bytes);
h.finish()
}
fn cast_analyzer_fingerprint() -> u64 {
use siphasher::sip::SipHasher13;
use std::hash::Hasher;
let mut files: Vec<PathBuf> = Vec::new();
for dir in [
"src/monitor/cast_analysis",
"src/vmm/cast_analysis_load",
"src/monitor/sdt_alloc",
"src/monitor/btf_render",
"src/monitor/bpf_map",
] {
println!("cargo:rerun-if-changed={dir}");
let path = std::path::Path::new(dir);
assert!(
path.is_dir(),
"cast-analysis fingerprint dir missing: {dir} (layout moved? update build.rs)"
);
collect_fingerprint_files(path, &mut files);
}
files.sort();
let mut h = SipHasher13::new_with_keys(0, 0);
for f in &files {
h.write(f.to_string_lossy().as_bytes());
let bytes = std::fs::read(f)
.unwrap_or_else(|e| panic!("read {} for analyzer fingerprint: {e}", f.display()));
h.write(&bytes);
}
h.finish()
}
fn cargo_lock_fingerprint() -> u64 {
use siphasher::sip::SipHasher13;
use std::hash::Hasher;
println!("cargo:rerun-if-changed=Cargo.lock");
let lock = std::fs::read_to_string("Cargo.lock")
.unwrap_or_else(|e| panic!("read Cargo.lock for dependency fingerprint: {e}"));
let mut h = SipHasher13::new_with_keys(0, 0);
h.write(lock.as_bytes());
h.finish()
}
fn collect_fingerprint_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_fingerprint_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs")
&& path.file_name().and_then(|n| n.to_str()) != Some("tests.rs")
{
out.push(path);
}
}
}