use std::{ env, ffi::OsString, fs, io::{BufRead as _, BufReader}, path::PathBuf, process::{Child, Command, Output, Stdio}, }; use cargo_metadata::{ Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind, }; use xtask::{exec, AYA_BUILD_INTEGRATION_BPF, LIBBPF_DIR}; /// This crate has a runtime dependency on artifacts produced by the `integration-ebpf` crate. This /// would be better expressed as one or more [artifact-dependencies][bindeps] but issues such as: /// /// * https://github.com/rust-lang/cargo/issues/12374 /// * https://github.com/rust-lang/cargo/issues/12375 /// * https://github.com/rust-lang/cargo/issues/12385 /// /// prevent their use for the time being. /// /// This file, along with the xtask crate, allows analysis tools such as `cargo check`, `cargo /// clippy`, and even `cargo build` to work as users expect. Prior to this file's existence, this /// crate's undeclared dependency on artifacts from `integration-ebpf` would cause build (and `cargo check`, /// and `cargo clippy`) failures until the user ran certain other commands in the workspace. Conversely, /// those same tools (e.g. cargo test --no-run) would produce stale results if run naively because /// they'd make use of artifacts from a previous build of `integration-ebpf`. /// /// Note that this solution is imperfect: in particular it has to balance correctness with /// performance; an environment variable is used to replace true builds of `integration-ebpf` with /// stubs to preserve the property that code generation and linking (in `integration-ebpf`) do not /// occur on metadata-only actions such as `cargo check` or `cargo clippy` of this crate. This means /// that naively attempting to `cargo test --no-run` this crate will produce binaries that fail at /// runtime because the stubs are inadequate for actually running the tests. /// /// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies fn main() { println!("cargo:rerun-if-env-changed={}", AYA_BUILD_INTEGRATION_BPF); let build_integration_bpf = env::var(AYA_BUILD_INTEGRATION_BPF) .as_deref() .map(str::parse) .map(Result::unwrap) .unwrap_or_default(); let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); let integration_ebpf_package = packages .into_iter() .find(|Package { name, .. }| name == "integration-ebpf") .unwrap(); let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); let manifest_dir = PathBuf::from(manifest_dir); let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = PathBuf::from(out_dir); let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); let target = if endian == "big" { "bpfeb" } else if endian == "little" { "bpfel" } else { panic!("unsupported endian={:?}", endian) }; const C_BPF: &[(&str, bool)] = &[ ("ext.bpf.c", false), ("iter.bpf.c", true), ("main.bpf.c", false), ("multimap-btf.bpf.c", false), ("reloc.bpf.c", true), ("text_64_64_reloc.c", false), ("variables_reloc.bpf.c", false), ]; if build_integration_bpf { let libbpf_dir = manifest_dir .parent() .unwrap() .parent() .unwrap() .join(LIBBPF_DIR); println!("cargo:rerun-if-changed={}", libbpf_dir.to_str().unwrap()); let libbpf_headers_dir = out_dir.join("libbpf_headers"); let mut includedir = OsString::new(); includedir.push("INCLUDEDIR="); includedir.push(&libbpf_headers_dir); exec( Command::new("make") .arg("-C") .arg(libbpf_dir.join("src")) .arg(includedir) .arg("install_headers"), ) .unwrap(); let bpf_dir = manifest_dir.join("bpf"); let mut target_arch = OsString::new(); target_arch.push("-D__TARGET_ARCH_"); let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); if arch == "x86_64" { target_arch.push("x86"); } else if arch == "aarch64" { target_arch.push("arm64"); } else { target_arch.push(&arch); }; // NB: libbpf's documentation suggests that vmlinux.h be generated by running `bpftool btf // dump file /sys/kernel/btf/vmlinux format c`; this allows CO-RE to work. // // However in our tests we do not make use of kernel data structures, and so any vmlinux.h // which defines the constants we need (e.g. `__u8`, `__u64`, `BPF_MAP_TYPE_ARRAY`, // `BPF_ANY`, `XDP_PASS`, `XDP_DROP`, etc.) will suffice. Since we already have a libbpf // submodule which happens to include such a file, we use it. let libbpf_vmlinux_dir = libbpf_dir.join(".github/actions/build-selftests"); let clang = || { let mut cmd = Command::new("clang"); cmd.arg("-nostdlibinc") .arg("-I") .arg(&libbpf_headers_dir) .arg("-I") .arg(&libbpf_vmlinux_dir) .args(["-g", "-O2", "-target", target, "-c"]) .arg(&target_arch); cmd }; for (src, build_btf) in C_BPF { let dst = out_dir.join(src).with_extension("o"); let src = bpf_dir.join(src); println!("cargo:rerun-if-changed={}", src.to_str().unwrap()); exec(clang().arg(&src).arg("-o").arg(&dst)).unwrap(); if *build_btf { let mut cmd = clang(); let mut child = cmd .arg("-DTARGET") .arg(&src) .args(["-o", "-"]) .stdout(Stdio::piped()) .spawn() .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); let Child { stdout, .. } = &mut child; let stdout = stdout.take().unwrap(); let dst = dst.with_extension("target.o"); let mut output = OsString::new(); output.push(".BTF="); output.push(dst); exec( // NB: objcopy doesn't support reading from stdin, so we have to use llvm-objcopy. Command::new("llvm-objcopy") .arg("--dump-section") .arg(output) .arg("-") .stdin(stdout), ) .unwrap(); let output = child .wait_with_output() .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); let Output { status, .. } = &output; assert_eq!(status.code(), Some(0), "{cmd:?} failed: {output:?}"); } } let target = format!("{target}-unknown-none"); let Package { manifest_path, .. } = integration_ebpf_package; let integration_ebpf_dir = manifest_path.parent().unwrap(); // We have a build-dependency on `integration-ebpf`, so cargo will automatically rebuild us // if `integration-ebpf`'s *library* target or any of its dependencies change. Since we // depend on `integration-ebpf`'s *binary* targets, that only gets us half of the way. This // stanza ensures cargo will rebuild us on changes to the binaries too, which gets us the // rest of the way. println!("cargo:rerun-if-changed={}", integration_ebpf_dir.as_str()); let mut cmd = Command::new("cargo"); cmd.args([ "build", "-Z", "build-std=core", "--bins", "--message-format=json", "--release", "--target", &target, ]); cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); // Workaround to make sure that the rust-toolchain.toml is respected. for key in ["RUSTUP_TOOLCHAIN", "RUSTC"] { cmd.env_remove(key); } cmd.current_dir(integration_ebpf_dir); // Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself. let ebpf_target_dir = out_dir.join("integration-ebpf"); cmd.arg("--target-dir").arg(&ebpf_target_dir); let mut child = cmd .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); let Child { stdout, stderr, .. } = &mut child; // Trampoline stdout to cargo warnings. let stderr = stderr.take().unwrap(); let stderr = BufReader::new(stderr); let stderr = std::thread::spawn(move || { for line in stderr.lines() { let line = line.unwrap(); println!("cargo:warning={line}"); } }); let stdout = stdout.take().unwrap(); let stdout = BufReader::new(stdout); let mut executables = Vec::new(); for message in Message::parse_stream(stdout) { #[allow(clippy::collapsible_match)] match message.expect("valid JSON") { Message::CompilerArtifact(Artifact { executable, target: Target { name, .. }, .. }) => { if let Some(executable) = executable { executables.push((name, executable.into_std_path_buf())); } } Message::CompilerMessage(CompilerMessage { message, .. }) => { for line in message.rendered.unwrap_or_default().split('\n') { println!("cargo:warning={line}"); } } Message::TextLine(line) => { println!("cargo:warning={line}"); } _ => {} } } let status = child .wait() .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); stderr.join().map_err(std::panic::resume_unwind).unwrap(); for (name, binary) in executables { let dst = out_dir.join(name); let _: u64 = fs::copy(&binary, &dst) .unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); } } else { for (src, build_btf) in C_BPF { let dst = out_dir.join(src).with_extension("o"); fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); if *build_btf { let dst = dst.with_extension("target.o"); fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); } } let Package { targets, .. } = integration_ebpf_package; for Target { name, kind, .. } in targets { if *kind != [TargetKind::Bin] { continue; } let dst = out_dir.join(name); fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); } } }