use crate::clang_info::ClangInfo;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use glob::glob;
use libbpf_cargo::SkeletonBuilder;
use libbpf_rs::Linker;
use std::collections::BTreeSet;
use std::env;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug)]
pub struct BpfBuilder {
clang: ClangInfo,
cflags: Vec<String>,
out_dir: PathBuf,
sources: BTreeSet<String>,
intf_input_output: Option<(String, String)>,
skel_input_name: Option<(String, String)>,
}
impl BpfBuilder {
const BPF_H_TAR: &'static [u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bpf_h.tar"));
fn install_bpf_h<P: AsRef<Path>>(dest: P) -> Result<()> {
let mut ar = tar::Archive::new(Self::BPF_H_TAR);
ar.unpack(dest)?;
Ok(())
}
fn determine_cflags<P>(clang: &ClangInfo, out_dir: P) -> Result<Vec<String>>
where
P: AsRef<Path> + std::fmt::Debug,
{
let bpf_h = out_dir
.as_ref()
.join("scx_utils-bpf_h")
.to_str()
.ok_or(anyhow!(
"{:?}/scx_utils-bph_h can't be converted to str",
&out_dir
))?
.to_string();
Self::install_bpf_h(&bpf_h)?;
let mut cflags = Vec::<String>::new();
cflags.append(&mut match env::var("BPF_BASE_CFLAGS") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => clang.determine_base_cflags()?,
});
cflags.append(&mut match env::var("BPF_EXTRA_CFLAGS_PRE_INCL") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => vec![],
});
cflags.push(format!(
"-I{}/arch/{}",
&bpf_h,
&clang.kernel_target().unwrap()
));
cflags.push(format!("-I{}", &bpf_h));
cflags.push(format!("-I{}/bpf-compat", &bpf_h));
cflags.append(&mut match env::var("BPF_EXTRA_CFLAGS_POST_INCL") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => vec![],
});
Ok(cflags)
}
pub fn new() -> Result<Self> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let clang = ClangInfo::new()?;
let cflags = match env::var("BPF_CFLAGS") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => Self::determine_cflags(&clang, &out_dir)?,
};
println!("scx_utils:clang={:?} {:?}", &clang, &cflags);
Ok(Self {
clang,
cflags,
out_dir,
sources: BTreeSet::new(),
intf_input_output: None,
skel_input_name: None,
})
}
pub fn enable_intf(&mut self, input: &str, output: &str) -> &mut Self {
self.intf_input_output = Some((input.into(), output.into()));
self
}
pub fn enable_skel(&mut self, input: &str, name: &str) -> &mut Self {
self.skel_input_name = Some((input.into(), name.into()));
self.sources.insert(input.into());
self
}
fn input_insert_deps(&self, deps: &mut BTreeSet<String>) -> () {
let (input, _) = match &self.intf_input_output {
Some(pair) => pair,
None => return,
};
deps.insert(input.to_string());
}
fn bindgen_bpf_intf(&self) -> Result<()> {
let (input, output) = match &self.intf_input_output {
Some(pair) => pair,
None => return Ok(()),
};
let bindings = bindgen::Builder::default()
.clang_args(
self.cflags
.iter()
.chain(["-target".into(), "bpf".into()].iter()),
)
.header(input)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.context("Unable to generate bindings")?;
bindings
.write_to_file(self.out_dir.join(output))
.context("Couldn't write bindings")
}
pub fn add_source(&mut self, input: &str) -> &mut Self {
self.sources.insert(input.into());
self
}
pub fn compile_link_gen(&mut self) -> Result<()> {
let input = match &self.skel_input_name {
Some((name, _)) => name,
None => return Ok(()),
};
let linkobj = self.out_dir.join("bpf.bpf.o");
let mut linker = Linker::new(&linkobj)?;
for filename in self.sources.iter() {
let name = Path::new(filename).file_name().unwrap().to_str().unwrap();
let obj = self.out_dir.join(name.replace(".bpf.c", ".bpf.o"));
let output = SkeletonBuilder::new()
.debug(true)
.source(filename)
.obj(&obj)
.clang(&self.clang.clang)
.clang_args(&self.cflags)
.build()?;
for line in String::from_utf8_lossy(output.stderr()).lines() {
println!("cargo:warning={}", line);
}
linker.add_file(&obj)?;
}
linker.link()?;
self.bindgen_bpf_intf()?;
let skel_path = self.out_dir.join("bpf_skel.rs");
SkeletonBuilder::new()
.obj(&linkobj)
.clang(&self.clang.clang)
.clang_args(&self.cflags)
.generate(&skel_path)?;
let mut deps = BTreeSet::new();
self.add_src_deps(&mut deps, input)?;
for filename in self.sources.iter() {
deps.insert(filename.to_string());
}
self.gen_cargo_reruns(Some(&deps))?;
Ok(())
}
fn gen_bpf_skel(&self, deps: &mut BTreeSet<String>) -> Result<()> {
let (input, name) = match &self.skel_input_name {
Some(pair) => pair,
None => return Ok(()),
};
let obj = self.out_dir.join(format!("{}.bpf.o", name));
let skel_path = self.out_dir.join(format!("{}_skel.rs", name));
let output = SkeletonBuilder::new()
.source(input)
.obj(&obj)
.clang(&self.clang.clang)
.clang_args(&self.cflags)
.build_and_generate(&skel_path)?;
for line in String::from_utf8_lossy(output.stderr()).lines() {
println!("cargo:warning={}", line);
}
self.add_src_deps(deps, input)?;
Ok(())
}
fn add_src_deps(&self, deps: &mut BTreeSet<String>, input: &str) -> Result<()> {
let c_path = PathBuf::from(input);
let dir = c_path
.parent()
.ok_or(anyhow!("Source {:?} doesn't have parent dir", c_path))?
.to_str()
.ok_or(anyhow!("Parent dir of {:?} isn't a UTF-8 string", c_path))?;
for path in glob(&format!("{}/*.[hc]", dir))?.filter_map(Result::ok) {
deps.insert(
path.to_str()
.ok_or(anyhow!("Path {:?} is not a valid string", path))?
.to_string(),
);
}
Ok(())
}
fn gen_cargo_reruns(&self, dependencies: Option<&BTreeSet<String>>) -> Result<()> {
println!("cargo:rerun-if-env-changed=BPF_CLANG");
println!("cargo:rerun-if-env-changed=BPF_CFLAGS");
println!("cargo:rerun-if-env-changed=BPF_BASE_CFLAGS");
println!("cargo:rerun-if-env-changed=BPF_EXTRA_CFLAGS_PRE_INCL");
println!("cargo:rerun-if-env-changed=BPF_EXTRA_CFLAGS_POST_INCL");
if let Some(deps) = dependencies {
for dep in deps.iter() {
println!("cargo:rerun-if-changed={}", dep);
}
};
for source in self.sources.iter() {
println!("cargo:rerun-if-changed={}", source);
}
Ok(())
}
pub fn build(&self) -> Result<()> {
let mut deps = BTreeSet::new();
self.input_insert_deps(&mut deps);
self.bindgen_bpf_intf()?;
self.gen_bpf_skel(&mut deps)?;
self.gen_cargo_reruns(Some(&deps))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use regex::Regex;
use sscanf::sscanf;
use crate::builder::ClangInfo;
#[test]
fn test_bpf_builder_new() {
let td = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("OUT_DIR", td.path()) };
let res = super::BpfBuilder::new();
assert!(res.is_ok(), "Failed to create BpfBuilder ({:?})", res);
}
#[test]
fn test_vmlinux_h_ver_sha1() {
let clang_info = ClangInfo::new().unwrap();
let mut ar = tar::Archive::new(super::BpfBuilder::BPF_H_TAR);
let mut found = false;
let pattern = Regex::new(r"arch\/.*\/vmlinux-.*.h").unwrap();
for entry in ar.entries().unwrap() {
let entry = entry.unwrap();
let file_name = entry.header().path().unwrap();
let file_name_str = file_name.to_string_lossy().to_owned();
if file_name_str.contains(&clang_info.kernel_target().unwrap()) {
found = true;
}
if !pattern.find(&file_name_str).is_some() {
continue;
}
println!("checking {file_name_str}");
let (arch, ver, sha1) =
sscanf!(file_name_str, "arch/{String}/vmlinux-v{String}-g{String}.h").unwrap();
println!(
"vmlinux.h: arch={:?} ver={:?} sha1={:?}",
&arch, &ver, &sha1,
);
assert!(regex::Regex::new(r"^([1-9][0-9]*\.[1-9][0-9][a-z0-9-]*)$")
.unwrap()
.is_match(&ver));
assert!(regex::Regex::new(r"^[0-9a-z]{12}$")
.unwrap()
.is_match(&sha1));
}
assert!(found);
}
}