Skip to content

Commit

Permalink
Extract aya-build for building eBPF crates
Browse files Browse the repository at this point in the history
We'll use this in the template and book to avoid duplicating all the
code.
  • Loading branch information
tamird committed Nov 28, 2024
1 parent ed963b2 commit b90ee6f
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 110 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"aya",
"aya-build",
"aya-log",
"aya-log-common",
"aya-log-parser",
Expand All @@ -25,6 +26,7 @@ resolver = "2"

default-members = [
"aya",
"aya-build",
"aya-log",
"aya-log-common",
"aya-log-parser",
Expand Down
11 changes: 11 additions & 0 deletions aya-build/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "aya-build"
version = "0.1.0"
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
edition.workspace = true

[dependencies]
cargo_metadata = { workspace = true }
138 changes: 138 additions & 0 deletions aya-build/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::{
env, fs,
io::{BufRead as _, BufReader},
path::PathBuf,
process::{Child, Command, Stdio},
};

// Re-export `cargo_metadata` to having to encode the version downstream and risk mismatches.
pub use cargo_metadata;
use cargo_metadata::{Artifact, CompilerMessage, Message, Package, Target};

/// Build binary artifacts produced by `packages`.
///
/// 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.
///
/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies
pub fn build_ebpf(packages: impl IntoIterator<Item = Package>) {
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)
};

let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();

let target = format!("{target}-unknown-none");

for Package {
name,
manifest_path,
..
} in packages
{
let dir = manifest_path.parent().unwrap();

// We have a build-dependency on `name`, so cargo will automatically rebuild us if `name`'s
// *library* target or any of its dependencies change. Since we depend on `name`'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={}", dir.as_str());

let mut cmd = Command::new("cargo");
cmd.args([
"+nightly",
"build",
"--package",
&name,
"-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 correct toolchain is used.
for key in ["RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}

// Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself.
let target_dir = out_dir.join(name);
cmd.arg("--target-dir").arg(&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}"));
}
}
}
2 changes: 1 addition & 1 deletion test/integration-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
xdpilone = { workspace = true }

[build-dependencies]
cargo_metadata = { workspace = true }
aya-build = { path = "../../aya-build" }
# TODO(https://github.com/rust-lang/cargo/issues/12375): this should be an artifact dependency, but
# it's not possible to tell cargo to use `-Z build-std` to build it. We cargo-in-cargo in the build
# script to build this, but we want to teach cargo about the dependecy so that cache invalidation
Expand Down
113 changes: 4 additions & 109 deletions test/integration-test/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,13 @@ 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 aya_build::cargo_metadata::{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`,
Expand All @@ -34,11 +22,11 @@ use xtask::{exec, AYA_BUILD_INTEGRATION_BPF, LIBBPF_DIR};
/// 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);

// TODO(https://github.com/rust-lang/cargo/issues/4001): generalize this and move it to
// aya-build if we can determine that we're in a check build.
let build_integration_bpf = env::var(AYA_BUILD_INTEGRATION_BPF)
.as_deref()
.map(str::parse)
Expand Down Expand Up @@ -177,100 +165,7 @@ fn main() {
}
}

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([
"+nightly",
"build",
"--package",
"integration-ebpf",
"-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 correct toolchain is used.
for key in ["RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}

// 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}"));
}
aya_build::build_ebpf([integration_ebpf_package]);
} else {
for (src, build_btf) in C_BPF {
let dst = out_dir.join(src).with_extension("o");
Expand Down
3 changes: 3 additions & 0 deletions xtask/public-api/aya-build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod aya_build
pub use aya_build::cargo_metadata
pub fn aya_build::build_ebpf(packages: impl core::iter::traits::collect::IntoIterator<Item = cargo_metadata::Package>)

0 comments on commit b90ee6f

Please sign in to comment.