diff --git a/libbpf-cargo/src/build.rs b/libbpf-cargo/src/build.rs index b439c4a4..20067b34 100644 --- a/libbpf-cargo/src/build.rs +++ b/libbpf-cargo/src/build.rs @@ -31,6 +31,192 @@ impl CompilationOutput { } } + +/// A helper for compiling BPF C code into a loadable BPF object file. +// TODO: Before exposing this functionality publicly, consider whether +// we should support per-input-file compiler arguments. +#[derive(Debug)] +pub(crate) struct BpfObjBuilder { + compiler: PathBuf, + compiler_args: Vec, +} + +impl BpfObjBuilder { + /// Specify which C compiler to use. + pub fn compiler>(&mut self, compiler: P) -> &mut Self { + self.compiler = compiler.as_ref().to_path_buf(); + self + } + + /// Pass additional arguments to the compiler when building the BPF object file. + pub fn compiler_args(&mut self, args: A) -> &mut Self + where + A: IntoIterator, + S: AsRef, + { + self.compiler_args = args + .into_iter() + .map(|arg| arg.as_ref().to_os_string()) + .collect(); + self + } + + /// We're essentially going to run: + /// + /// clang -g -O2 -target bpf -c -D__TARGET_ARCH_$(ARCH) runqslower.bpf.c -o runqslower.bpf.o + /// + /// for each prog. + fn compile_single( + src: &Path, + dst: &Path, + compiler: &Path, + compiler_args: &[OsString], + ) -> Result { + debug!("Building {}", src.display()); + + let mut cmd = Command::new(compiler.as_os_str()); + cmd.args(compiler_args); + + cmd.arg("-g") + .arg("-O2") + .arg("-target") + .arg("bpf") + .arg("-c") + .arg(src.as_os_str()) + .arg("-o") + .arg(dst); + + let output = cmd + .output() + .with_context(|| format!("failed to execute `{}`", compiler.display()))?; + if !output.status.success() { + let err = Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string())) + .with_context(|| { + format!( + "command `{}` failed ({})", + format_command(&cmd), + output.status + ) + }) + .with_context(|| { + format!("failed to compile {} from {}", dst.display(), src.display()) + }); + return err; + } + + Ok(CompilationOutput { + stderr: output.stderr, + }) + } + + fn with_compiler_args(&self, f: F) -> Result + where + F: FnOnce(&[OsString]) -> Result, + { + let mut compiler_args = self.compiler_args.clone(); + + let header_parent_dir = tempdir().context("failed to create temporary directory")?; + let header_dir = extract_libbpf_headers_to_disk(header_parent_dir.path()) + .context("failed to extract libbpf header")?; + + if let Some(dir) = header_dir { + compiler_args.push(OsString::from("-I")); + compiler_args.push(dir.into_os_string()); + } + + // Explicitly disable stack protector logic, which doesn't work with + // BPF. See https://lkml.org/lkml/2020/2/21/1000. + compiler_args.push(OsString::from("-fno-stack-protector")); + + if !compiler_args + .iter() + .any(|arg| arg.to_string_lossy().contains("__TARGET_ARCH_")) + { + // We may end up being invoked by a build script, in which case + // `CARGO_CFG_TARGET_ARCH` would represent the target architecture. + let arch = env::var("CARGO_CFG_TARGET_ARCH"); + let arch = arch.as_deref().unwrap_or(ARCH); + let arch = match arch { + "x86_64" => "x86", + "aarch64" => "arm64", + "powerpc64" => "powerpc", + "s390x" => "s390", + "riscv64" => "riscv", + "loongarch64" => "loongarch", + "sparc64" => "sparc", + "mips64" => "mips", + x => x, + }; + compiler_args.push(format!("-D__TARGET_ARCH_{arch}").into()); + } + + f(&compiler_args) + } + + /// Build a BPF object file from a set of input files. + pub fn build_many(&mut self, srcs: S, dst: &Path) -> Result> + where + S: IntoIterator, + P: AsRef, + { + let obj_dir = tempdir().context("failed to create temporary directory")?; + let mut linker = libbpf_rs::Linker::new(dst) + .context("failed to instantiate libbpf object file linker")?; + + let output = self.with_compiler_args(|compiler_args| { + srcs.into_iter() + .map(|src| { + let src = src.as_ref(); + let tmp_dst = obj_dir.path().join(src.file_name().with_context(|| { + format!( + "input path `{}` does not have a proper file name", + src.display() + ) + })?); + + let output = Self::compile_single(src, &tmp_dst, &self.compiler, compiler_args) + .with_context(|| format!("failed to compile `{}`", src.display()))?; + + linker + .add_file(tmp_dst) + .context("failed to add object file to BPF linker")?; + Ok(output) + }) + .collect::>() + })?; + + // The resulting object file may contain DWARF information + // that references system specific and temporary paths. That + // can render our generated skeletons unstable, potentially + // making them unsuitable for inclusion in version control + // systems. Linking has the side effect of stripping this + // information. + linker.link().context("failed to link object file")?; + + Ok(output) + } + + /// Build a BPF object file. + pub fn build(&mut self, src: &Path, dst: &Path) -> Result { + self.build_many([src], dst).map(|vec| { + // SANITY: We pass in a single file we `build_many` is + // guaranteed to produce as many outputs as input + // files; so there must be one. + vec.into_iter().next().unwrap() + }) + } +} + +impl Default for BpfObjBuilder { + fn default() -> Self { + Self { + compiler: "clang".into(), + compiler_args: Vec::new(), + } + } +} + + fn check_progs(objs: &[UnprocessedObj]) -> Result<()> { let mut set = HashSet::with_capacity(objs.len()); for obj in objs { @@ -80,25 +266,6 @@ fn extract_libbpf_headers_to_disk(_target_dir: &Path) -> Result> Ok(None) } -/// Strip DWARF information from the provided BPF object file. -/// -/// We rely on the `libbpf` linker here, which removes debug information as a -/// side-effect. -fn strip_dwarf_info(file: &Path) -> Result<()> { - let mut temp_file = file.as_os_str().to_os_string(); - temp_file.push(".tmp"); - - fs::rename(file, &temp_file).context("Failed to rename compiled BPF object file")?; - - let mut linker = - libbpf_rs::Linker::new(file).context("Failed to instantiate libbpf object file linker")?; - linker - .add_file(temp_file) - .context("Failed to add object file to BPF linker")?; - linker.link().context("Failed to link object file")?; - Ok(()) -} - /// Concatenate a command and its arguments into a single string. fn concat_command(command: C, args: A) -> OsString where @@ -122,117 +289,6 @@ fn format_command(command: &Command) -> String { concat_command(prog, args).to_string_lossy().to_string() } -/// We're essentially going to run: -/// -/// clang -g -O2 -target bpf -c -D__TARGET_ARCH_$(ARCH) runqslower.bpf.c -o runqslower.bpf.o -/// -/// for each prog. -fn compile_one( - source: &Path, - out: &Path, - clang: &Path, - clang_args: &[OsString], -) -> Result { - debug!("Building {}", source.display()); - - let mut cmd = Command::new(clang.as_os_str()); - cmd.args(clang_args); - - if !clang_args - .iter() - .any(|arg| arg.to_string_lossy().contains("__TARGET_ARCH_")) - { - // We may end up being invoked by a build script, in which case - // `CARGO_CFG_TARGET_ARCH` would represent the target architecture. - let arch = env::var("CARGO_CFG_TARGET_ARCH"); - let arch = arch.as_deref().unwrap_or(ARCH); - let arch = match arch { - "x86_64" => "x86", - "aarch64" => "arm64", - "powerpc64" => "powerpc", - "s390x" => "s390", - "riscv64" => "riscv", - "loongarch64" => "loongarch", - "sparc64" => "sparc", - "mips64" => "mips", - x => x, - }; - cmd.arg(format!("-D__TARGET_ARCH_{arch}")); - } - - cmd.arg("-g") - .arg("-O2") - .arg("-target") - .arg("bpf") - .arg("-c") - .arg(source.as_os_str()) - .arg("-o") - .arg(out); - - let output = cmd.output().context("Failed to execute clang")?; - if !output.status.success() { - let err = Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string())) - .with_context(|| { - format!( - "Command `{}` failed ({})", - format_command(&cmd), - output.status - ) - }) - .with_context(|| { - format!( - "Failed to compile {} from {}", - out.display(), - source.display() - ) - }); - return err; - } - - // Compilation with clang may contain DWARF information that references - // system specific and temporary paths. That can render our generated - // skeletons unstable, potentially rendering them unsuitable for inclusion - // in version control systems. So strip this information. - strip_dwarf_info(out) - .with_context(|| format!("Failed to strip object file {}", out.display()))?; - - Ok(CompilationOutput { - stderr: output.stderr, - }) -} - -fn compile( - objs: &[UnprocessedObj], - clang: &Path, - mut clang_args: Vec, - target_dir: &Path, -) -> Result> { - let header_dir = extract_libbpf_headers_to_disk(target_dir)?; - if let Some(dir) = header_dir { - clang_args.push(OsString::from("-I")); - clang_args.push(dir.into_os_string()); - } - - objs.iter() - .map(|obj| -> Result<_> { - let stem = obj.path.file_stem().with_context(|| { - format!( - "Could not calculate destination name for obj={}", - obj.path.display() - ) - })?; - - let mut dest_name = stem.to_os_string(); - dest_name.push(".o"); - - let mut dest_path = obj.out.to_path_buf(); - dest_path.push(&dest_name); - fs::create_dir_all(&obj.out)?; - compile_one(&obj.path, &dest_path, clang, &clang_args) - }) - .collect::>() -} - fn extract_clang_or_default(clang: Option<&PathBuf>) -> PathBuf { match clang { Some(c) => c.into(), @@ -241,12 +297,12 @@ fn extract_clang_or_default(clang: Option<&PathBuf>) -> PathBuf { } } -pub fn build( +pub fn build_project( manifest_path: Option<&PathBuf>, clang: Option<&PathBuf>, clang_args: Vec, ) -> Result<()> { - let (target_dir, to_compile) = metadata::get(manifest_path)?; + let (_target_dir, to_compile) = metadata::get(manifest_path)?; if !to_compile.is_empty() { debug!("Found bpf progs to compile:"); @@ -260,30 +316,36 @@ pub fn build( check_progs(&to_compile)?; let clang = extract_clang_or_default(clang); - compile(&to_compile, &clang, clang_args, &target_dir).context("Failed to compile progs")?; - - Ok(()) -} + let _output = to_compile + .iter() + .map(|obj| { + let stem = obj.path.file_stem().with_context(|| { + format!( + "Could not calculate destination name for obj={}", + obj.path.display() + ) + })?; -// Only used in libbpf-cargo library -pub(crate) fn build_single( - source: &Path, - out: &Path, - clang: Option<&PathBuf>, - mut clang_args: Vec, -) -> Result { - let clang = extract_clang_or_default(clang); - let header_parent_dir = tempdir()?; - let header_dir = extract_libbpf_headers_to_disk(header_parent_dir.path())?; + let mut dest_name = stem.to_os_string(); + dest_name.push(".o"); - if let Some(dir) = header_dir { - clang_args.push(OsString::from("-I")); - clang_args.push(dir.into_os_string()); - } + let mut dest_path = obj.out.to_path_buf(); + dest_path.push(&dest_name); + fs::create_dir_all(&obj.out)?; - // Explicitly disable stack protector logic, which doesn't work with - // BPF. See https://lkml.org/lkml/2020/2/21/1000. - clang_args.push(OsString::from("-fno-stack-protector")); + BpfObjBuilder::default() + .compiler(&clang) + .compiler_args(&clang_args) + .build(&obj.path, &dest_path) + .with_context(|| { + format!( + "failed to compile `{}` into `{}`", + obj.path.display(), + dest_path.display() + ) + }) + }) + .collect::, _>>()?; - compile_one(source, out, &clang, &clang_args) + Ok(()) } diff --git a/libbpf-cargo/src/lib.rs b/libbpf-cargo/src/lib.rs index d5e30f56..c000713c 100644 --- a/libbpf-cargo/src/lib.rs +++ b/libbpf-cargo/src/lib.rs @@ -87,6 +87,9 @@ mod metadata; #[cfg(test)] mod test; +use build::BpfObjBuilder; + + /// `SkeletonBuilder` builds and generates a single skeleton. /// /// This interface is meant to be used in build scripts. @@ -229,14 +232,16 @@ impl SkeletonBuilder { self.dir = Some(dir); } - build::build_single( - source, - // Unwrap is safe here since we guarantee that obj.is_some() above - self.obj.as_ref().unwrap(), - self.clang.as_ref(), - self.clang_args.clone(), - ) - .with_context(|| format!("failed to build `{}`", source.display())) + let mut builder = BpfObjBuilder::default(); + if let Some(clang) = &self.clang { + builder.compiler(clang); + } + builder.compiler_args(&self.clang_args); + + // SANITY: Unwrap is safe here since we guarantee that obj.is_some() above. + builder + .build(source, self.obj.as_ref().unwrap()) + .with_context(|| format!("failed to build `{}`", source.display())) } // Generate a skeleton at path `output` without building BPF programs. @@ -263,7 +268,7 @@ impl SkeletonBuilder { #[doc(hidden)] pub mod __private { pub mod build { - pub use crate::build::build; + pub use crate::build::build_project; } pub mod gen { pub use crate::gen::gen; diff --git a/libbpf-cargo/src/main.rs b/libbpf-cargo/src/main.rs index 402d84a6..74f75c43 100644 --- a/libbpf-cargo/src/main.rs +++ b/libbpf-cargo/src/main.rs @@ -126,7 +126,7 @@ fn main() -> Result<()> { clang_path, clang_args, }, - } => build::build(manifest_path.as_ref(), clang_path.as_ref(), clang_args), + } => build::build_project(manifest_path.as_ref(), clang_path.as_ref(), clang_args), Command::Gen { manifest_path, rustfmt_path, diff --git a/libbpf-cargo/src/make.rs b/libbpf-cargo/src/make.rs index f0d89068..fbdb0e34 100644 --- a/libbpf-cargo/src/make.rs +++ b/libbpf-cargo/src/make.rs @@ -20,7 +20,8 @@ pub fn make( rustfmt_path: Option<&PathBuf>, ) -> Result<()> { debug!("Compiling BPF objects"); - build::build(manifest_path, clang, clang_args).context("Failed to compile BPF objects")?; + build::build_project(manifest_path, clang, clang_args) + .context("Failed to compile BPF objects")?; debug!("Generating skeletons"); gen::gen(manifest_path, None, rustfmt_path).context("Failed to generate skeletons")?; diff --git a/libbpf-cargo/src/test.rs b/libbpf-cargo/src/test.rs index aeced8fe..03ea5c2b 100644 --- a/libbpf-cargo/src/test.rs +++ b/libbpf-cargo/src/test.rs @@ -20,7 +20,7 @@ use tempfile::NamedTempFile; use tempfile::TempDir; use test_log::test; -use crate::build::build; +use crate::build::build_project; use crate::gen::GenBtf; use crate::gen::GenStructOps; use crate::make::make; @@ -140,17 +140,17 @@ fn test_build_default() { let (_dir, proj_dir, cargo_toml) = setup_temp_project(); // No bpf progs yet - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); // Add prog dir create_dir(proj_dir.join("src/bpf")).expect("failed to create prog dir"); - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); // Add a prog let _prog_file = File::create(proj_dir.join("src/bpf/prog.bpf.c")).expect("failed to create prog file"); - build(Some(&cargo_toml), None, Vec::new()).unwrap(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap(); // Validate generated object file validate_bpf_o(proj_dir.as_path().join("target/bpf/prog.bpf.o").as_path()); @@ -168,7 +168,7 @@ fn test_build_invalid_prog() { File::create(proj_dir.join("src/bpf/prog.bpf.c")).expect("failed to create prog file"); writeln!(prog_file, "1").expect("write to prog file failed"); - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); } #[test] @@ -187,14 +187,14 @@ fn test_build_custom() { .expect("write to Cargo.toml failed"); // No bpf progs yet - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); // Add a prog create_dir(proj_dir.join("src/other_bpf_dir")).expect("failed to create prog dir"); let _prog_file = File::create(proj_dir.join("src/other_bpf_dir/prog.bpf.c")) .expect("failed to create prog file"); - build(Some(&cargo_toml), None, Vec::new()).unwrap(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap(); // Validate generated object file validate_bpf_o( @@ -224,13 +224,13 @@ fn test_unknown_metadata_section() { // Add prog dir create_dir(proj_dir.join("src/bpf")).expect("failed to create prog dir"); - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); // Add a prog let _prog_file = File::create(proj_dir.join("src/bpf/prog.bpf.c")).expect("failed to create prog file"); - build(Some(&cargo_toml), None, Vec::new()).unwrap(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap(); // Validate generated object file validate_bpf_o(proj_dir.as_path().join("target/bpf/prog.bpf.o").as_path()); @@ -242,15 +242,15 @@ fn test_enforce_file_extension() { // Add prog dir create_dir(proj_dir.join("src/bpf")).expect("failed to create prog dir"); - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); let _prog_file = File::create(proj_dir.join("src/bpf/prog_BAD_EXTENSION.c")) .expect("failed to create prog file"); - build(Some(&cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap_err(); let _prog_file_again = File::create(proj_dir.join("src/bpf/prog_GOOD_EXTENSION.bpf.c")) .expect("failed to create prog file"); - build(Some(&cargo_toml), None, Vec::new()).unwrap(); + build_project(Some(&cargo_toml), None, Vec::new()).unwrap(); } #[test] @@ -258,7 +258,7 @@ fn test_build_workspace() { let (_dir, _, workspace_cargo_toml, proj_one_dir, proj_two_dir) = setup_temp_workspace(); // No bpf progs yet - build(Some(&workspace_cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&workspace_cargo_toml), None, Vec::new()).unwrap_err(); // Create bpf prog for project one create_dir(proj_one_dir.join("src/bpf")).expect("failed to create prog dir"); @@ -270,7 +270,7 @@ fn test_build_workspace() { let _prog_file_2 = File::create(proj_two_dir.join("src/bpf/prog2.bpf.c")) .expect("failed to create prog file 2"); - build(Some(&workspace_cargo_toml), None, Vec::new()).unwrap(); + build_project(Some(&workspace_cargo_toml), None, Vec::new()).unwrap(); } #[test] @@ -287,7 +287,7 @@ fn test_build_workspace_collision() { let _prog_file_2 = File::create(proj_two_dir.join("src/bpf/prog.bpf.c")) .expect("failed to create prog file 2"); - build(Some(&workspace_cargo_toml), None, Vec::new()).unwrap_err(); + build_project(Some(&workspace_cargo_toml), None, Vec::new()).unwrap_err(); } #[test] @@ -1193,7 +1193,7 @@ fn build_btf_mmap(prog_text: &str) -> Mmap { add_vmlinux_header(&proj_dir); // Build the .bpf.o - build(Some(&cargo_toml), None, Vec::new()).expect("failed to compile"); + build_project(Some(&cargo_toml), None, Vec::new()).expect("failed to compile"); let obj = OpenOptions::new() .read(true)