Replace xtask with build scripts

Adapt https://github.com/aya-rs/aya/commit/3d463a3 and subsequent work
to the template. This has worked very well for us in the main project,
and our users should get the same hotness.
reviewable/pr124/r1
Tamir Duberstein 7 months ago
parent 4da4bf4729
commit 51da029036
No known key found for this signature in database

@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

@ -10,6 +10,7 @@ aya-log = { version = "0.2.1", default-features = false }
aya-log-ebpf = { version = "0.1.1", default-features = false } aya-log-ebpf = { version = "0.1.1", default-features = false }
anyhow = { version = "1", default-features = false } anyhow = { version = "1", default-features = false }
cargo_metadata = { version = "0.18.0", default-features = false }
# `std` feature is currently required to build `clap`. # `std` feature is currently required to build `clap`.
# #
# See https://github.com/clap-rs/clap/blob/61f5ee5/clap_builder/src/lib.rs#L15. # See https://github.com/clap-rs/clap/blob/61f5ee5/clap_builder/src/lib.rs#L15.
@ -18,6 +19,7 @@ env_logger = { version = "0.11.5", default-features = false }
libc = { version = "0.2.159", default-features = false } libc = { version = "0.2.159", default-features = false }
log = { version = "0.4.22", default-features = false } log = { version = "0.4.22", default-features = false }
tokio = { version = "1.40.0", default-features = false } tokio = { version = "1.40.0", default-features = false }
which = { version = "6.0.0", default-features = false }
[profile.dev] [profile.dev]
opt-level = 3 opt-level = 3

@ -4,29 +4,7 @@
1. Install bpf-linker: `cargo install bpf-linker` 1. Install bpf-linker: `cargo install bpf-linker`
## Build eBPF ## Build, Run
```bash Use `cargo build`, `cargo run`, etc. as normal. Cargo build scripts are used to
cargo xtask build-ebpf automatically build the eBPF correctly and include it in the program.
```
To perform a release build you can use the `--release` flag.
You may also change the target architecture with the `--target` flag.
## Build Userspace
```bash
cargo build
```
## Build eBPF and Userspace
```bash
cargo xtask build
```
## Run
```bash
RUST_LOG=info cargo xtask run
```

@ -51,12 +51,11 @@ esac
cargo generate --path "${TEMPLATE_DIR}" -n test -d program_type="${PROG_TYPE}" ${ADDITIONAL_ARGS} cargo generate --path "${TEMPLATE_DIR}" -n test -d program_type="${PROG_TYPE}" ${ADDITIONAL_ARGS}
pushd test pushd test
cargo xtask build cargo build --package test
cargo xtask build --release cargo build --package test --release
# We cannot run clippy over the whole workspace at once due to feature unification. Since both test # We cannot run clippy over the whole workspace at once due to feature unification. Since both test
# and test-ebpf both depend on test-common and test activates test-common's aya dependency, we end # and test-ebpf depend on test-common and test activates test-common's aya dependency, we end up
# up trying to compile the panic handler twice: once from the bpf program, and again from std via # trying to compile the panic handler twice: once from the bpf program, and again from std via aya.
# aya.
cargo clippy --exclude test-ebpf --all-targets --workspace -- --deny warnings cargo clippy --exclude test-ebpf --all-targets --workspace -- --deny warnings
cargo clippy --package test-ebpf --all-targets -- --deny warnings cargo clippy --package test-ebpf --all-targets -- --deny warnings
popd popd

@ -2,7 +2,3 @@
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, default-features = true, features = ["derive"] }

@ -1,41 +0,0 @@
use std::process::Command;
use anyhow::Context as _;
use clap::Parser;
use crate::build_ebpf::{build_ebpf, Architecture, Options as BuildOptions};
#[derive(Debug, Parser)]
pub struct Options {
/// Set the endianness of the BPF target
#[clap(default_value = "bpfel-unknown-none", long)]
pub bpf_target: Architecture,
/// Build and run the release target
#[clap(long)]
pub release: bool,
}
/// Build our ebpf program and the userspace program.
pub fn build(opts: Options) -> Result<(), anyhow::Error> {
let Options {
bpf_target,
release,
} = opts;
// Build our ebpf program.
build_ebpf(BuildOptions {
target: bpf_target,
release,
})?;
// Build our userspace program.
let mut cmd = Command::new("cargo");
cmd.arg("build");
if release {
cmd.arg("--release");
}
let status = cmd.status().context("failed to build userspace")?;
anyhow::ensure!(status.success(), "failed to build userspace program: {}", status);
Ok(())
}

@ -1,68 +0,0 @@
use std::process::Command;
use anyhow::Context as _;
use clap::Parser;
#[derive(Debug, Clone)]
pub enum Architecture {
BpfEl,
BpfEb,
}
impl Architecture {
pub fn as_str(&self) -> &'static str {
match self {
Architecture::BpfEl => "bpfel-unknown-none",
Architecture::BpfEb => "bpfeb-unknown-none",
}
}
}
impl std::str::FromStr for Architecture {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"bpfel-unknown-none" => Architecture::BpfEl,
"bpfeb-unknown-none" => Architecture::BpfEb,
_ => return Err("invalid target"),
})
}
}
impl std::fmt::Display for Architecture {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Parser)]
pub struct Options {
/// Set the endianness of the BPF target
#[clap(default_value = "bpfel-unknown-none", long)]
pub target: Architecture,
/// Build the release target
#[clap(long)]
pub release: bool,
}
pub fn build_ebpf(opts: Options) -> Result<(), anyhow::Error> {
let Options { target, release } = opts;
let mut cmd = Command::new("cargo");
cmd.current_dir("{{project-name}}-ebpf")
// Command::new creates a child process which inherits all env variables. This means env
// vars set by the cargo xtask command are also inherited. RUSTUP_TOOLCHAIN is removed so
// the rust-toolchain.toml file in the -ebpf folder is honored.
.env_remove("RUSTUP_TOOLCHAIN")
.args(["build", "--target", target.as_str()]);
if release {
cmd.arg("--release");
}
let status = cmd.status().context("failed to build bpf program")?;
anyhow::ensure!(status.success(), "failed to build bpf program: {}", status);
Ok(())
}

@ -0,0 +1 @@
pub const AYA_BUILD_EBPF: &str = "AYA_BUILD_EBPF";

@ -1,36 +0,0 @@
mod build_ebpf;
mod build;
mod run;
use std::process::exit;
use clap::Parser;
#[derive(Debug, Parser)]
pub struct Options {
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, Parser)]
enum Command {
BuildEbpf(build_ebpf::Options),
Build(build::Options),
Run(run::Options),
}
fn main() {
let opts = Options::parse();
use Command::*;
let ret = match opts.command {
BuildEbpf(opts) => build_ebpf::build_ebpf(opts),
Run(opts) => run::run(opts),
Build(opts) => build::build(opts),
};
if let Err(e) = ret {
eprintln!("{e:#}");
exit(1);
}
}

@ -1,55 +0,0 @@
use std::process::Command;
use anyhow::Context as _;
use clap::Parser;
use crate::{build::{build, Options as BuildOptions}, build_ebpf::Architecture};
#[derive(Debug, Parser)]
pub struct Options {
/// Set the endianness of the BPF target
#[clap(default_value = "bpfel-unknown-none", long)]
pub bpf_target: Architecture,
/// Build and run the release target
#[clap(long)]
pub release: bool,
/// The command used to wrap your application
#[clap(short, long, default_value = "sudo -E")]
pub runner: String,
/// Arguments to pass to your application
#[clap(name = "args", last = true)]
pub run_args: Vec<String>,
}
/// Build and run the project
pub fn run(opts: Options) -> Result<(), anyhow::Error> {
// Build our ebpf program and the project
build(BuildOptions{
bpf_target: opts.bpf_target,
release: opts.release,
}).context("Error while building project")?;
// profile we are building (release or debug)
let profile = if opts.release { "release" } else { "debug" };
let bin_path = format!("target/{profile}/{{project-name}}");
// arguments to pass to the application
let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect();
// configure args
let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect();
args.push(bin_path.as_str());
args.append(&mut run_args);
// run the command
let status = Command::new(args.first().expect("No first argument"))
.args(args.iter().skip(1))
.status()
.expect("failed to run the command");
if !status.success() {
anyhow::bail!("Failed to run `{}`", args.join(" "));
}
Ok(())
}

@ -9,6 +9,10 @@ edition = "2021"
aya-ebpf = { workspace = true } aya-ebpf = { workspace = true }
aya-log-ebpf = { workspace = true } aya-log-ebpf = { workspace = true }
[build-dependencies]
which = { workspace = true }
xtask = { path = "../xtask" }
[[bin]] [[bin]]
name = "{{ project-name }}" name = "{{ project-name }}"
path = "src/main.rs" path = "src/main.rs"

@ -0,0 +1,30 @@
use std::env;
use which::which;
use xtask::AYA_BUILD_EBPF;
/// Building this crate has an undeclared dependency on the `bpf-linker` binary. This would be
/// better expressed by [artifact-dependencies][bindeps] but issues such as
/// https://github.com/rust-lang/cargo/issues/12385 make their use impractical for the time being.
///
/// This file implements an imperfect solution: it causes cargo to rebuild the crate whenever the
/// mtime of `which bpf-linker` changes. Note that possibility that a new bpf-linker is added to
/// $PATH ahead of the one used as the cache key still exists. Solving this in the general case
/// would require rebuild-if-changed-env=PATH *and* rebuild-if-changed={every-directory-in-PATH}
/// which would likely mean far too much cache invalidation.
///
/// [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_EBPF);
let build_ebpf = env::var(AYA_BUILD_EBPF)
.as_deref()
.map(str::parse)
.map(Result::unwrap)
.unwrap_or_default();
if build_ebpf {
let bpf_linker = which("bpf-linker").unwrap();
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
}
}

@ -0,0 +1,3 @@
#![no_std]
// This file exists to enable the library target.

@ -18,6 +18,23 @@ tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "net"
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
{% endif -%} {% endif -%}
[build-dependencies]
cargo_metadata = { workspace = true }
# 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
# works properly.
#
# Note also that https://github.com/rust-lang/cargo/issues/10593 occurs when `target = ...` is added
# to an artifact dependency; it seems possible to work around that by setting `resolver = "1"` in
# Cargo.toml in the workspace root.
#
# Finally note that *any* usage of `artifact = ...` in *any* Cargo.toml in the workspace breaks
# workflows with stable cargo; stable cargo outright refuses to load manifests that use unstable
# features.
{{project-name}}-ebpf = { path = "../{{project-name}}-ebpf" }
xtask = { path = "../xtask"}
[[bin]] [[bin]]
name = "{{project-name}}" name = "{{project-name}}"
path = "src/main.rs" path = "src/main.rs"

@ -0,0 +1,173 @@
use std::{
env,
fs,
io::{BufRead as _, BufReader},
path::PathBuf,
process::{Child, Command, Stdio},
};
use cargo_metadata::{
Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target,
};
use xtask::AYA_BUILD_EBPF;
/// This crate has a runtime dependency on artifacts produced by the `{{project-name}}-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 `{{project-name}}-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
/// `{{project-name}}-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 `{{project-name}}-ebpf`
/// with stubs to preserve the property that code generation and linking (in
/// `{{project-name}}-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_EBPF);
let build_integration_bpf = env::var(AYA_BUILD_EBPF)
.as_deref()
.map(str::parse)
.map(Result::unwrap)
.unwrap_or_default();
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages
.into_iter()
.find(|Package { name, .. }| name == "{{project-name}}-ebpf")
.unwrap();
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)
};
if build_integration_bpf {
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let target = format!("{target}-unknown-none");
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
// We have a build-dependency on `{{project-name}}-ebpf`, so cargo will automatically rebuild us
// if `{{project-name}}-ebpf`'s *library* target or any of its dependencies change. Since we
// depend on `{{project-name}}-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={}", 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(ebpf_dir);
// Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself.
let ebpf_target_dir = out_dir.join("{{project-name}}-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 {
let Package { targets, .. } = 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}"));
}
}
}

@ -85,14 +85,7 @@ async fn main() -> Result<(), anyhow::Error> {
// runtime. This approach is recommended for most real-world use cases. If you would // runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can // like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead. // reach for `Bpf::load_file` instead.
#[cfg(debug_assertions)] let mut ebpf = Ebpf::load(include_bytes_aligned!(concat!(env!("OUT_DIR"), "/{{project-name}}")))?;
let mut ebpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/{{project-name}}"
))?;
#[cfg(not(debug_assertions))]
let mut ebpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/{{project-name}}"
))?;
if let Err(e) = EbpfLogger::init(&mut ebpf) { if let Err(e) = EbpfLogger::init(&mut ebpf) {
// This can happen if you remove all log statements from your eBPF program. // This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e); warn!("failed to initialize eBPF logger: {}", e);

Loading…
Cancel
Save