123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- 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,
- };
- 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),
- ("main.bpf.c", false),
- ("multimap-btf.bpf.c", false),
- ("reloc.bpf.c", true),
- ("text_64_64_reloc.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, .. }) => {
- println!("cargo:warning={message}");
- }
- 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 != ["bin"] {
- continue;
- }
- let dst = out_dir.join(name);
- fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}"));
- }
- }
- }
|