diff --git a/rust/scx_utils/Cargo.toml b/rust/scx_utils/Cargo.toml index 2c2d8f1..c67a4a1 100644 --- a/rust/scx_utils/Cargo.toml +++ b/rust/scx_utils/Cargo.toml @@ -11,10 +11,12 @@ description = "Utilities for sched_ext schedulers" anyhow = "1.0" bindgen = "0.69" glob = "0.3" +lazy_static = "1.4" libbpf-cargo = "0.21" regex = "1.10" sscanf = "0.4" tar = "0.4" +version-compare = "0.1" [build-dependencies] tar = "0.4" diff --git a/rust/scx_utils/src/bpf_builder.rs b/rust/scx_utils/src/bpf_builder.rs index 98202ee..676d770 100644 --- a/rust/scx_utils/src/bpf_builder.rs +++ b/rust/scx_utils/src/bpf_builder.rs @@ -4,49 +4,45 @@ // GNU General Public License version 2. use anyhow::anyhow; +use anyhow::bail; use anyhow::Context; use anyhow::Result; use glob::glob; use libbpf_cargo::SkeletonBuilder; use sscanf::sscanf; use std::collections::BTreeSet; +use std::collections::HashMap; use std::env; use std::path::Path; use std::path::PathBuf; +use std::process::Command; -const BPF_H_TAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bpf_h.tar")); - -fn install_bpf_h>(dest: P) -> Result<()> { - let mut ar = tar::Archive::new(BPF_H_TAR); - ar.unpack(dest)?; - Ok(()) -} - -fn vmlinux_h_version() -> (String, String) { - let mut ar = tar::Archive::new(BPF_H_TAR); - - for file in ar.entries().unwrap() { - let file = file.unwrap(); - if file.header().path().unwrap() != Path::new("vmlinux/vmlinux.h") { - continue; - } - - let name = file - .link_name() - .unwrap() - .unwrap() - .to_string_lossy() - .to_string(); - - return sscanf!(name, "vmlinux-v{String}-g{String}.h").unwrap(); - } - - panic!("vmlinux/vmlinux.h not found"); +lazy_static::lazy_static! { + // Map clang archs to the __TARGET_ARCH list in + // tools/lib/bpf/bpf_tracing.h in the kernel tree. + static ref ARCH_MAP: HashMap<&'static str, &'static str> = vec![ + ("x86", "x86"), + ("x86_64", "x86"), + ("s390", "s390"), + ("arm", "arm"), + ("aarch64", "arm64"), + ("mips", "mips"), + ("mips64", "mips"), + ("ppc32", "powerpc"), + ("ppc64", "powerpc"), + ("sparc", "sparc"), + ("sparcv9", "sparc"), + ("riscv32", "riscv"), + ("riscv64", "riscv"), + ("arc", "arc"), // unsure this is supported + ("loongarch64", "loongarch"), // ditto + ].into_iter().collect(); } +#[derive(Debug)] pub struct BpfBuilder { - clang: String, - cflags: String, + clang: (String, String, String), // (clang, ver, arch) + cflags: Vec, out_dir: PathBuf, intf_input_output: Option<(String, String)>, @@ -55,11 +51,209 @@ pub struct BpfBuilder { } impl BpfBuilder { + fn find_clang() -> Result<(String, String, String)> { + let clang = env::var("BPF_CLANG").unwrap_or("clang".into()); + let output = Command::new(&clang) + .args(["--version"]) + .output() + .with_context(|| format!("Failed to run \"{} --version\"", &clang))?; + + let stdout = String::from_utf8(output.stdout)?; + let (mut ver, mut arch) = (None, None); + for line in stdout.lines() { + if let Ok(v) = sscanf!(line, "clang version {String}") { + // Version could be followed by (URL SHA1). Only take + // the first word. + ver = Some(v.split_whitespace().next().unwrap().to_string()); + continue; + } + if let Ok(v) = sscanf!(line, "Target: {String}") { + arch = Some(v.split('-').next().unwrap().to_string()); + continue; + } + } + + let (ver, arch) = ( + ver.ok_or(anyhow!("Failed to read clang version"))?, + arch.ok_or(anyhow!("Failed to read clang target arch"))?, + ); + + if version_compare::compare(&ver, "16") == Ok(version_compare::Cmp::Lt) { + bail!("clang < 16 loses high 32 bits of 64 bit enums when compiling BPF"); + } + if version_compare::compare(&ver, "17") == Ok(version_compare::Cmp::Lt) { + println!( + "cargo:warning=clang >= 17 recommended ({:?} ver={:?})", + &clang, &ver + ); + } + + Ok((clang, ver, arch)) + } + + fn determine_base_cflags( + (clang, _ver, arch): &(String, String, String), + ) -> Result> { + // Determine kernel target arch. + let kernel_target = match ARCH_MAP.get(arch.as_str()) { + Some(v) => v, + None => bail!("CPU arch {:?} not found in ARCH_MAP", &arch), + }; + + // Determine system includes. + let output = Command::new(&clang) + .args(["-v", "-E", "-"]) + .output() + .with_context(|| format!("Failed to run \"{} -v -E - < /dev/null", &clang))?; + let stderr = String::from_utf8(output.stderr)?; + + let mut sys_incls = None; + for line in stderr.lines() { + if line == "#include <...> search starts here:" { + sys_incls = Some(vec![]); + continue; + } + if sys_incls.is_none() { + continue; + } + if line == "End of search list." { + break; + } + + sys_incls.as_mut().unwrap().push(line.trim()); + } + let sys_incls = match sys_incls { + Some(v) => v, + None => bail!("Failed to find system includes from {:?}", &clang), + }; + + // Determine endian. + let output = Command::new(&clang) + .args(["-dM", "-E", "-"]) + .output() + .with_context(|| format!("Failed to run \"{} -dM E - < /dev/null", &clang))?; + let stdout = String::from_utf8(output.stdout)?; + + let mut endian = None; + for line in stdout.lines() { + match sscanf!(line, "#define __BYTE_ORDER__ {str}") { + Ok(v) => { + endian = Some(match v { + "__ORDER_LITTLE_ENDIAN__" => "little", + "__ORDER_BIG_ENDIAN__" => "big", + v => bail!("Unknown __BYTE_ORDER__ {:?}", v), + }); + break; + } + _ => {} + } + } + let endian = match endian { + Some(v) => v, + None => bail!("Failed to find __BYTE_ORDER__ from {:?}", &clang), + }; + + // Assemble cflags. + let mut cflags: Vec = + ["-g", "-O2", "-Wall", "-Wno-compare-distinct-pointer-types'"] + .into_iter() + .map(|x| x.into()) + .collect(); + cflags.push(format!("-D__TARGET_ARCH_{}", &kernel_target)); + cflags.push("-mcpu=v3".into()); + cflags.push(format!("-m{}-endian", endian)); + cflags.append( + &mut sys_incls + .into_iter() + .flat_map(|x| ["-idirafter".into(), x.into()]) + .collect(), + ); + Ok(cflags) + } + + const BPF_H_TAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bpf_h.tar")); + + fn install_bpf_h>(dest: P) -> Result<()> { + let mut ar = tar::Archive::new(Self::BPF_H_TAR); + ar.unpack(dest)?; + Ok(()) + } + + pub fn vmlinux_h_version() -> (String, String) { + let mut ar = tar::Archive::new(Self::BPF_H_TAR); + + for file in ar.entries().unwrap() { + let file = file.unwrap(); + if file.header().path().unwrap() != Path::new("vmlinux/vmlinux.h") { + continue; + } + + let name = file + .link_name() + .unwrap() + .unwrap() + .to_string_lossy() + .to_string(); + + return sscanf!(name, "vmlinux-v{String}-g{String}.h").unwrap(); + } + + panic!("vmlinux/vmlinux.h not found"); + } + + fn determine_cflags

(clang: &(String, String, String), out_dir: P) -> Result> + where + P: AsRef + 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::::new(); + + cflags.append(&mut match env::var("BPF_BASE_CFLAGS") { + Ok(v) => v.split_whitespace().map(|x| x.into()).collect(), + _ => Self::determine_base_cflags(&clang)?, + }); + + 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{}/vmlinux", &bpf_h)); + cflags.push(format!("-I{}/common", &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 { + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + + let clang = Self::find_clang()?; + 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: env::var("BPF_CLANG")?, - cflags: env::var("BPF_CFLAGS")?, - out_dir: PathBuf::from(env::var("OUT_DIR")?), + clang, + cflags, + out_dir, intf_input_output: None, skel_input_name: None, @@ -85,6 +279,14 @@ impl BpfBuilder { self } + fn cflags_string(&self) -> String { + self.cflags + .iter() + .map(|x| x.as_str()) + .collect::>() + .join(" ") + } + fn bindgen_bpf_intf(&self, deps: &mut BTreeSet) -> Result<()> { let (input, output) = match &self.intf_input_output { Some(pair) => pair, @@ -98,7 +300,7 @@ impl BpfBuilder { // you build up options for the resulting bindings. let bindings = bindgen::Builder::default() // Should run clang with the same -I options as BPF compilation. - .clang_args(self.cflags.split_whitespace()) + .clang_args(&self.cflags) // The input header we would like to generate bindings for. .header(input) // Tell cargo to invalidate the built crate whenever any of the @@ -113,10 +315,10 @@ impl BpfBuilder { } fn gen_bpf_skel(&self, deps: &mut BTreeSet) -> Result<()> { - let (input, name) = match &self.skel_input_name { - Some(pair) => pair, - None => return Ok(()), - }; + 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)); @@ -124,8 +326,8 @@ impl BpfBuilder { SkeletonBuilder::new() .source(input) .obj(&obj) - .clang(&self.clang) - .clang_args(&self.cflags) + .clang(&self.clang.0) + .clang_args(self.cflags_string()) .build_and_generate(&skel_path)?; match &self.skel_deps { @@ -174,22 +376,16 @@ mod tests { use std::io::BufReader; #[test] - fn test_install_bpf_h() { - let dir = concat!(env!("OUT_DIR"), "/test_install_bpf_h"); - super::install_bpf_h(dir).unwrap(); - - let vmlinux_h = File::open(format!("{}/vmlinux/vmlinux.h", dir)).unwrap(); - assert_eq!( - BufReader::new(vmlinux_h).lines().next().unwrap().unwrap(), - "#ifndef __VMLINUX_H__" - ); + fn test_bpf_builder_new() { + let res = super::BpfBuilder::new(); + assert!(res.is_ok(), "Failed to create BpfBuilder ({:?})", &res); } #[test] fn test_vmlinux_h_version() { - let (ver, sha1) = super::vmlinux_h_version(); + let (ver, sha1) = super::BpfBuilder::vmlinux_h_version(); - println!("test_vmlinux_h_version: ver={:?} sha1={:?}", &ver, &sha1,); + println!("vmlinux.h: ver={:?} sha1={:?}", &ver, &sha1,); assert!( regex::Regex::new(r"^[1-9][0-9]*\.[1-9][0-9]*(\.[1-9][0-9]*)?$")