use anyhow::{Context, Result, anyhow, bail};
use flate2::read::GzDecoder;
use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
thread,
};
use tar::Archive;
const BPFTOOL_VERSION: &str = "7.6.0";
const BPFTOOL_ARM64_SHA: &str = "b53ff306dc1d51d64f13a2b717f6ba5687a3613b87277ad0108464cf7b886cb7";
const BPFTOOL_AMD64_SHA: &str = "51ffd3dd4f46fdc46736433a971e828dc70835c6b18ad20cabffd10abcf00358";
const BPFTOOL_RELEASE_URL: &str = "https://github.com/libbpf/bpftool/releases/download";
const LIBSCAP_REPO: &str = "https://github.com/falcosecurity/libs.git";
const LIBSCAP_CHECKOUT_SHA: &str = "7250ae96aa8878385f85a5643a43459d3d32fca4";
fn main() {
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_FULL_BINDINGS");
let full_bindings_enabled = env::var("CARGO_FEATURE_FULL_BINDINGS").is_ok();
if !full_bindings_enabled {
println!("cargo:rerun-if-changed=build.rs");
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let repo_dir = out_dir.join("external-libscap");
let target = std::env::var("TARGET").unwrap();
let is_musl = target.contains("musl");
let bpftool_download = spawn_bpftool_download(&repo_dir);
if !repo_dir.exists()
&& let Err(err) = clone_libscap(&repo_dir)
{
let _ = fs::remove_dir_all(&repo_dir);
panic!("Failed to fetch libscap sources: {err}");
}
let relative_exe =
install_bpftool(&repo_dir, bpftool_download).expect("must fetch bpftool build dep");
let launcher = env::var("CMAKE_C_COMPILER_LAUNCHER")
.ok()
.filter(|launcher| !launcher.is_empty());
let mut modern_clang_shim = None;
let mut libbpf_cc_shim = None;
if cfg!(unix)
&& let Some(launcher) = &launcher
{
let clang = env::var("MODERN_CLANG_EXE").unwrap_or_else(|_| "clang".to_string());
modern_clang_shim = write_shim(&out_dir, "launcher-clang", launcher, &clang).ok();
libbpf_cc_shim = write_shim(&out_dir, "launcher-cc", launcher, "cc").ok();
}
patch_libbpf_build(&repo_dir, libbpf_cc_shim.as_deref());
let mut cmake_config = cmake::Config::new(&repo_dir);
cmake_config
.define("USE_BUNDLED_DEPS", "ON")
.define("MODERN_BPFTOOL_EXE", relative_exe)
.define("BUILD_LIBSCAP_GVISOR", "OFF")
.define("CREATE_TEST_TARGETS", "OFF")
.define("BUILD_LIBSCAP_MODERN_BPF", "ON")
.define("ENABLE_PIC", "ON")
.define("MUSL_OPTIMIZED_BUILD", if is_musl { "ON" } else { "OFF" });
if let Some(shim) = &modern_clang_shim {
cmake_config.define("MODERN_CLANG_EXE", shim);
} else if let Ok(clang_exe) = env::var("MODERN_CLANG_EXE") {
println!("cargo:info=Using MODERN_CLANG_EXE={}", clang_exe);
cmake_config.define("MODERN_CLANG_EXE", clang_exe);
}
let dst = cmake_config.build_target("scap").build();
println!(
"cargo:rustc-link-search=native={}/build/libbpf-prefix/src/libbpf-build/build",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/libpman",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/libscap",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/libscap/linux",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/libscap/engine/noop",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/libscap/engine/modern_bpf",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/_deps/libelf_elftoolchain-build/libelf",
dst.display()
);
println!(
"cargo:rustc-link-search=native={}/build/zlib-prefix/src/zlib",
dst.display()
);
println!("cargo:rustc-link-lib=static=scap_engine_modern_bpf");
println!("cargo:rustc-link-lib=static=scap_engine_noop");
println!("cargo:rustc-link-lib=static=scap");
println!("cargo:rustc-link-lib=static=pman");
println!("cargo:rustc-link-lib=static=scap_engine_util");
println!("cargo:rustc-link-lib=static=scap_platform");
println!("cargo:rustc-link-lib=static=scap_platform_util");
println!("cargo:rustc-link-lib=static=scap_event_schema");
println!("cargo:rustc-link-lib=static=driver_event_schema");
println!("cargo:rustc-link-lib=static=scap_error");
println!("cargo:rustc-link-lib=static=bpf");
println!("cargo:rustc-link-lib=static=elf");
println!("cargo:rustc-link-lib=static=z");
#[derive(Debug)]
struct StrumMacroMungeCallback;
impl bindgen::callbacks::ParseCallbacks for StrumMacroMungeCallback {
fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec<String> {
if info.kind == bindgen::callbacks::TypeKind::Enum {
vec!["EnumIter".into(), "FromRepr".into(), "Display".into()]
} else {
vec![]
}
}
}
let clang_args = vec![
format!("-I{}", repo_dir.display()),
format!("-I{}/userspace", repo_dir.display()),
format!("-I{}/userspace/libscap", repo_dir.display()),
format!("-I{}/build", dst.display()),
format!("-I{}/build/libscap", dst.display()),
format!("-I{}/build/uthash-prefix/src/uthash/src", dst.display()),
];
fn get_builder(clang_args: &[String]) -> bindgen::Builder {
bindgen::Builder::default()
.header("inc/libscap.h")
.clang_args(clang_args)
.derive_default(true)
.derive_debug(true)
.derive_copy(true)
.size_t_is_usize(true)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: true,
})
.vtable_generation(true)
.generate_pure_virtual_functions(true)
.generate_comments(true)
.wrap_unsafe_ops(true)
.rust_edition(bindgen::RustEdition::Edition2024)
.parse_callbacks(Box::new(StrumMacroMungeCallback))
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
}
let enum_bindings = get_builder(&clang_args)
.allowlist_type("ppm_sc_code")
.allowlist_type("ppm_param_type")
.allowlist_type("ppm_event_code")
.allowlist_type("ppm_event_flags")
.allowlist_type("scap_fd_type")
.allowlist_type("scap_l4_proto")
.generate()
.expect("Unable to generate enum bindings");
let enum_file_path = PathBuf::from("src/enums.rs");
enum_bindings
.write_to_file(&enum_file_path)
.expect("Couldn't write enum bindings!");
let const_bindings = get_builder(&clang_args)
.allowlist_var("PPM_.*")
.generate()
.expect("Unable to generate const bindings");
let const_file_path = PathBuf::from("src/consts.rs");
const_bindings
.write_to_file(&const_file_path)
.expect("Couldn't write const bindings!");
let bindings = get_builder(&clang_args)
.raw_line(
"use super::types::{ppm_param_type, ppm_sc_code, ppm_event_code, ppm_event_flags, scap_fd_type, scap_l4_proto};",
)
.blocklist_type("ppm_sc_code")
.blocklist_type("ppm_param_type")
.blocklist_type("ppm_event_code")
.blocklist_type("ppm_event_flags")
.blocklist_type("scap_fd_type")
.blocklist_type("scap_l4_proto")
.blocklist_var("PPM_.*")
.generate()
.expect("Unable to generate bindings");
let out_path = out_dir.join("bindings.rs");
bindings
.write_to_file(&out_path)
.expect("Couldn't write bindings!");
println!("cargo:rerun-if-changed=build.rs");
}
fn clone_libscap(repo_dir: &Path) -> Result<()> {
let repo = repo_dir.to_str().unwrap();
run_git(&["init", repo])?;
run_git(&["-C", repo, "remote", "add", "origin", LIBSCAP_REPO])?;
run_git(&[
"-C",
repo,
"fetch",
"--depth=1",
"origin",
LIBSCAP_CHECKOUT_SHA,
])?;
run_git(&[
"-C",
repo,
"-c",
"advice.detachedHead=false",
"checkout",
"--detach",
"FETCH_HEAD",
])?;
Ok(())
}
fn run_git(args: &[&str]) -> Result<()> {
let status = Command::new("git").args(args).status()?;
if !status.success() {
bail!("`git {}` failed with {}", args.join(" "), status);
}
Ok(())
}
type BpftoolDownload = Option<thread::JoinHandle<Result<Vec<u8>>>>;
fn spawn_bpftool_download(repo_dir: &Path) -> BpftoolDownload {
let (arch, expected_sha) = get_arch_sha();
let archive = repo_dir.join("bpftool").join(archive_name(&arch));
if archive_checksum_ok(&archive, &expected_sha) {
return None;
}
Some(thread::spawn(move || {
download_bpftool_bytes(&arch, &expected_sha)
}))
}
fn install_bpftool(repo_dir: &Path, download: BpftoolDownload) -> Result<String> {
let (arch, _) = get_arch_sha();
let bpftool_dir = repo_dir.join("bpftool");
let archive_name = archive_name(&arch);
if let Some(handle) = download {
let bytes = handle
.join()
.map_err(|_| anyhow!("bpftool download thread panicked"))??;
let _ = fs::remove_dir_all(&bpftool_dir);
fs::create_dir_all(&bpftool_dir)?;
fs::write(bpftool_dir.join(&archive_name), &bytes)?;
}
extract_bpftool(&archive_name, &bpftool_dir)
}
fn archive_name(arch: &str) -> String {
format!("bpftool-v{}-{}.tar.gz", BPFTOOL_VERSION, arch)
}
fn archive_checksum_ok(archive: &Path, expected_sha: &str) -> bool {
fs::read(archive).is_ok_and(|bytes| sha256::digest(bytes.as_slice()) == expected_sha)
}
fn write_shim(out_dir: &Path, name: &str, launcher: &str, tool: &str) -> Result<PathBuf> {
let path = out_dir.join(name);
fs::write(&path, format!("#!/bin/sh\nexec {launcher} {tool} \"$@\"\n"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms)?;
}
Ok(path)
}
fn patch_libbpf_build(repo_dir: &Path, cc_shim: Option<&Path>) {
let module = repo_dir.join("cmake/modules/libbpf.cmake");
let Ok(text) = fs::read_to_string(&module) else {
return;
};
let jobs = env::var("NUM_JOBS").unwrap_or_else(|_| "1".to_string());
let mut replacement = format!("make -j{jobs}");
if let Some(shim) = cc_shim {
replacement.push_str(&format!(" CC={}", shim.display()));
}
replacement.push_str(" BUILD_STATIC_ONLY=y");
let patched = text.replace("make BUILD_STATIC_ONLY=y", &replacement);
if patched != text {
let _ = fs::write(&module, patched);
} else if !text.contains("make -j") {
println!(
"cargo:warning=libbpf.cmake BUILD_COMMAND pattern not found; \
libbpf will build single-threaded without the compiler launcher"
);
}
}
fn get_arch_sha() -> (String, String) {
let target = env::var("TARGET").expect("rust builds should have a target");
if !target.contains("linux") {
panic!("only linux targets currently supported")
}
if target.starts_with("x86_64") {
("amd64".to_string(), BPFTOOL_AMD64_SHA.to_string())
} else if target.starts_with("aarch64") {
("arm64".to_string(), BPFTOOL_ARM64_SHA.to_string())
} else {
panic!("unsupported target arch")
}
}
fn download_bpftool_bytes(arch: &str, expected_sha: &str) -> Result<Vec<u8>> {
let url = format!(
"{}/v{}/{}",
BPFTOOL_RELEASE_URL,
BPFTOOL_VERSION,
archive_name(arch)
);
let content = ureq::get(&url)
.call()
.with_context(|| format!("downloading bpftool from {url}"))?
.body_mut()
.with_config()
.limit(64 * 1024 * 1024)
.read_to_vec()
.with_context(|| format!("reading bpftool response body from {url}"))?;
let actual_checksum = sha256::digest(content.as_slice());
if actual_checksum != expected_sha {
bail!(
"checksum mismatch downloading {}: expected: {}, got: {}",
url,
expected_sha,
actual_checksum
);
}
Ok(content)
}
fn extract_bpftool(archive_name: &str, bpftool_dir: &Path) -> Result<String> {
let tarball_path = bpftool_dir.join(archive_name);
let tarball_file = fs::File::open(&tarball_path)?;
let tar_gz = GzDecoder::new(tarball_file);
let mut archive = Archive::new(tar_gz);
let bpftool_binary = bpftool_dir.join("bpftool");
archive.unpack(bpftool_dir)?;
let full_path = fs::canonicalize(bpftool_binary)?;
if !full_path.exists() {
bail!("could not extract bpftool");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&full_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&full_path, perms)?;
}
let file_name = full_path
.file_name()
.expect("has filename")
.to_str()
.unwrap();
let parent_name = full_path
.parent()
.unwrap()
.file_name()
.unwrap()
.to_str()
.unwrap();
let relative_path = format!("{}/{}", parent_name, file_name);
Ok(relative_path)
}