use anyhow::{Result, bail};
use flate2::read::GzDecoder;
use std::{
env, fs,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
};
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");
if !repo_dir.exists() {
let status = Command::new("git")
.args(["clone", LIBSCAP_REPO, repo_dir.to_str().unwrap()])
.status()
.expect("Failed to clone repository");
if !status.success() {
panic!("Failed to clone repository");
}
let checkout_dir = fs::canonicalize(repo_dir.clone()).unwrap();
let status = Command::new("git")
.args(["checkout", LIBSCAP_CHECKOUT_SHA])
.current_dir(checkout_dir)
.status()
.expect("Failed to checkout ref {}");
if !status.success() {
panic!("Failed to checkout repository");
}
}
let relative_exe = fetch_bpftool(&repo_dir).expect("must fetch bpftool build dep");
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 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_.*")
.emit_clang_ast()
.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 fetch_bpftool(out_dir: &Path) -> Result<String> {
let bpftool_dir = out_dir.join("bpftool/");
let (arch, expected_sha) = get_arch_sha();
let archive_name = format!("bpftool-v{}-{}.tar.gz", BPFTOOL_VERSION, arch);
let bpftool_archive = bpftool_dir.join(&archive_name);
let should_download = if bpftool_archive.exists() {
let mut file = fs::File::open(&bpftool_archive)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let actual_checksum = sha256::digest(&contents);
if actual_checksum == expected_sha {
false
} else {
println!("cargo:error=bpftool checksum mismatch, redownloading");
let _ = fs::remove_dir_all(&bpftool_dir);
true
}
} else {
let _ = fs::remove_dir_all(&bpftool_dir);
true
};
let _ = fs::create_dir_all(&bpftool_dir);
if should_download {
download_bpftool(&expected_sha, &archive_name, &bpftool_dir)?;
}
extract_bpftool(&archive_name, &bpftool_dir)
}
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(expected_sha: &str, archive_name: &str, bpftool_dir: &Path) -> Result<()> {
let url = format!(
"{}/v{}/{}",
BPFTOOL_RELEASE_URL, BPFTOOL_VERSION, archive_name
);
let response = reqwest::blocking::get(&url)?;
if !response.status().is_success() {
bail!(format!(
"failed to download bpftool: HTTP {:?}",
response.status()
));
}
let content = response.bytes()?;
let actual_checksum = sha256::digest(content.as_ref());
if actual_checksum != expected_sha {
bail!(
"checksum mismatch downloading {}: expected: {}, got: {}",
url,
expected_sha,
actual_checksum
);
}
let tarball_path = bpftool_dir.join(archive_name);
let mut tarball_file = fs::File::create(&tarball_path)?;
tarball_file.write_all(&content)?;
Ok(())
}
fn extract_bpftool(archive_name: &str, bpftool_dir: &PathBuf) -> 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)
}