diff --git a/.cargo/config b/.cargo/config index 757fd081..dbd1a2b4 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,5 +1,7 @@ [alias] xtask = "run --package xtask --" +build-bpfel = "build -Zbuild-std=core --target=bpfel-unknown-none" +build-bpfeb = "build -Zbuild-std=core --target=bpfeb-unknown-none" [target.armv7-unknown-linux-gnueabi] linker = "arm-linux-gnueabi-gcc" diff --git a/.github/workflows/build-aya-bpf.yml b/.github/workflows/build-aya-bpf.yml index e520833d..a29d7c09 100644 --- a/.github/workflows/build-aya-bpf.yml +++ b/.github/workflows/build-aya-bpf.yml @@ -31,59 +31,19 @@ jobs: - uses: actions-rs/toolchain@v1 with: toolchain: nightly + components: rust-src override: true - uses: Swatinem/rust-cache@v1 - name: Prereqs - run: cargo install cross --git https://github.com/cross-rs/cross + run: cargo install bpf-linker - name: Build env: CARGO_CFG_BPF_TARGET_ARCH: ${{ matrix.arch }} run: | - pushd bpf - cargo build --workspace --exclude aya-bpf-macros --verbose - popd - - - name: Run tests - env: - CARGO_CFG_BPF_TARGET_ARCH: ${{ matrix.arch }} - run: | - pushd bpf - cargo test --workspace --exclude aya-bpf-macros --verbose - popd - - build-macros: - strategy: - matrix: - arch: - - x86_64-unknown-linux-gnu - - aarch64-unknown-linux-gnu - - armv7-unknown-linux-gnueabi - - riscv64gc-unknown-none-elf - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - - - uses: Swatinem/rust-cache@v1 - - - name: Prereqs - run: cargo install cross --git https://github.com/cross-rs/cross - - - name: Build bpf macros - run: | - pushd bpf - cross build -p aya-bpf-macros --verbose - popd - - - name: Test bpf macros - run: | - pushd bpf - RUST_BACKTRACE=full cross test -p aya-bpf-macros --verbose - popd + cargo build-bpfel -p aya-bpf --verbose + cargo build-bpfeb -p aya-bpf --verbose + cargo build-bpfel -p aya-log-ebpf --verbose + cargo build-bpfeb -p aya-log-ebpf --verbose diff --git a/.github/workflows/build-aya.yml b/.github/workflows/build-aya.yml index 9d523875..8f57b1de 100644 --- a/.github/workflows/build-aya.yml +++ b/.github/workflows/build-aya.yml @@ -21,19 +21,28 @@ jobs: - x86_64-unknown-linux-gnu - aarch64-unknown-linux-gnu - armv7-unknown-linux-gnueabi - - riscv64gc-unknown-none-elf + - riscv64gc-unknown-linux-gnu runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v1 - name: Prereqs run: cargo install cross --git https://github.com/cross-rs/cross - name: Build - run: cross build --verbose + run: cross build --verbose --target ${{matrix.arch}} - name: Run test - run: RUST_BACKTRACE=full cross test --verbose + env: + RUST_BACKTRACE: full + run: | + cross test --verbose --target ${{matrix.arch}} test: runs-on: ubuntu-20.04 @@ -60,12 +69,10 @@ jobs: sudo apt-get -qy install linux-tools-common qemu-system-x86 cloud-image-utils openssh-client libelf-dev gcc-multilib cargo install bpf-linker - - name: Lint integration tests run: | cargo xtask build-integration-test-ebpf --libbpf-dir ./libbpf cargo clippy -p integration-test -- --deny warnings - cargo clippy -p integration-test-macros -- --deny warnings - name: Run integration tests run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ffb10c76..afe47cf0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,22 +30,11 @@ jobs: - name: Check formatting run: | cargo fmt --all -- --check - (cd bpf && cargo fmt --all -- --check) - (cd test/integration-ebpf && cargo fmt --all -- --check) - name: Run clippy run: | - cargo clippy -p aya -- --deny warnings - cargo clippy -p aya-gen -- --deny warnings - cargo clippy -p xtask -- --deny warnings - (cd bpf && cargo clippy -p aya-bpf -- --deny warnings) - (cd test/integration-ebpf && cargo clippy -- --deny warnings) + cargo clippy --workspace --exclude integration-test -- --deny warnings - name: Run miri - env: - MIRIFLAGS: -Zmiri-disable-stacked-borrows run: | - cargo miri test --all-targets - pushd bpf - cargo miri test - popd + cargo miri test --all-targets \ No newline at end of file diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json index 4b796ccb..07c919f9 100644 --- a/.vim/coc-settings.json +++ b/.vim/coc-settings.json @@ -1,4 +1,4 @@ { - "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml", "test/integration-ebpf/Cargo.toml"], - "rust-analyzer.checkOnSave.allTargets": false + "rust-analyzer.checkOnSave.allTargets": false, + "rust-analyzer.checkOnSave.command": "clippy" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b796ccb..07c919f9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml", "test/integration-ebpf/Cargo.toml"], - "rust-analyzer.checkOnSave.allTargets": false + "rust-analyzer.checkOnSave.allTargets": false, + "rust-analyzer.checkOnSave.command": "clippy" } diff --git a/Cargo.toml b/Cargo.toml index c5a9a2e0..61c3b7df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,22 @@ [workspace] -members = ["aya", "aya-gen", "test/integration-test", "test/integration-test-macros", "xtask"] -default-members = ["aya", "aya-gen"] +members = [ + "aya", "aya-gen", "aya-log", "aya-log-common", "test/integration-test", "test/integration-test-macros", "xtask", + # macros + "aya-bpf-macros", "aya-log-ebpf-macros", + # ebpf crates + "bpf/aya-bpf", "bpf/aya-bpf-bindings", "bpf/aya-log-ebpf", "test/integration-ebpf" +] +default-members = ["aya", "aya-gen", "aya-log", "aya-bpf-macros", "aya-log-ebpf-macros"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[profile.dev.package.integration-ebpf] +opt-level = 2 +overflow-checks = false + +[profile.release.package.integration-ebpf] +debug = 2 \ No newline at end of file diff --git a/bpf/aya-bpf-macros/Cargo.toml b/aya-bpf-macros/Cargo.toml similarity index 87% rename from bpf/aya-bpf-macros/Cargo.toml rename to aya-bpf-macros/Cargo.toml index 9c43f7bd..929098ee 100644 --- a/bpf/aya-bpf-macros/Cargo.toml +++ b/aya-bpf-macros/Cargo.toml @@ -13,4 +13,4 @@ quote = "1.0" syn = {version = "1.0", features = ["full"]} [dev-dependencies] -aya-bpf = { path = "../aya-bpf" } +aya-bpf = { path = "../bpf/aya-bpf" } diff --git a/bpf/aya-bpf-macros/src/expand.rs b/aya-bpf-macros/src/expand.rs similarity index 100% rename from bpf/aya-bpf-macros/src/expand.rs rename to aya-bpf-macros/src/expand.rs diff --git a/bpf/aya-bpf-macros/src/lib.rs b/aya-bpf-macros/src/lib.rs similarity index 100% rename from bpf/aya-bpf-macros/src/lib.rs rename to aya-bpf-macros/src/lib.rs diff --git a/aya-log-common/Cargo.toml b/aya-log-common/Cargo.toml new file mode 100644 index 00000000..4b50e2a1 --- /dev/null +++ b/aya-log-common/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "aya-log-common" +version = "0.1.11-dev.0" +description = "A logging library for eBPF programs." +keywords = ["ebpf", "bpf", "log", "logging"] +license = "MIT OR Apache-2.0" +authors = ["The Aya Contributors"] +repository = "https://github.com/aya-rs/aya-log" +documentation = "https://docs.rs/aya-log" +edition = "2018" + +[features] +default = [] +userspace = [ "aya" ] + +[dependencies] +aya = { path = "../aya", version = "0.11.0", optional=true } + +[lib] +path = "src/lib.rs" diff --git a/aya-log-common/release.toml b/aya-log-common/release.toml new file mode 100644 index 00000000..bf6eaefc --- /dev/null +++ b/aya-log-common/release.toml @@ -0,0 +1 @@ +shared-version = true diff --git a/aya-log-common/src/lib.rs b/aya-log-common/src/lib.rs new file mode 100644 index 00000000..0aa63846 --- /dev/null +++ b/aya-log-common/src/lib.rs @@ -0,0 +1,185 @@ +#![no_std] + +use core::{cmp, mem, ptr}; + +pub const LOG_BUF_CAPACITY: usize = 8192; + +pub const LOG_FIELDS: usize = 7; + +#[repr(usize)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +pub enum Level { + /// The "error" level. + /// + /// Designates very serious errors. + Error = 1, + /// The "warn" level. + /// + /// Designates hazardous situations. + Warn, + /// The "info" level. + /// + /// Designates useful information. + Info, + /// The "debug" level. + /// + /// Designates lower priority information. + Debug, + /// The "trace" level. + /// + /// Designates very low priority, often extremely verbose, information. + Trace, +} + +#[repr(usize)] +#[derive(Copy, Clone, Debug)] +pub enum RecordField { + Target = 1, + Level, + Module, + File, + Line, + NumArgs, + Log, +} + +#[repr(usize)] +#[derive(Copy, Clone, Debug)] +pub enum ArgType { + I8, + I16, + I32, + I64, + I128, + Isize, + + U8, + U16, + U32, + U64, + U128, + Usize, + + F32, + F64, + + Str, +} + +#[cfg(feature = "userspace")] +mod userspace { + use super::*; + + unsafe impl aya::Pod for RecordField {} + unsafe impl aya::Pod for ArgType {} +} + +struct TagLenValue<'a, T> { + tag: T, + value: &'a [u8], +} + +impl<'a, T> TagLenValue<'a, T> +where + T: Copy, +{ + #[inline(always)] + pub(crate) fn new(tag: T, value: &'a [u8]) -> TagLenValue<'a, T> { + TagLenValue { tag, value } + } + + pub(crate) fn write(&self, mut buf: &mut [u8]) -> Result { + let size = mem::size_of::() + mem::size_of::() + self.value.len(); + let remaining = cmp::min(buf.len(), LOG_BUF_CAPACITY); + // Check if the size doesn't exceed the buffer bounds. + if size > remaining { + return Err(()); + } + + unsafe { ptr::write_unaligned(buf.as_mut_ptr() as *mut _, self.tag) }; + buf = &mut buf[mem::size_of::()..]; + + unsafe { ptr::write_unaligned(buf.as_mut_ptr() as *mut _, self.value.len()) }; + buf = &mut buf[mem::size_of::()..]; + + let len = cmp::min(buf.len(), self.value.len()); + // The verifier isn't happy with `len` being unbounded, so compare it + // with `LOG_BUF_CAPACITY`. + if len > LOG_BUF_CAPACITY { + return Err(()); + } + buf[..len].copy_from_slice(&self.value[..len]); + Ok(size) + } +} + +pub trait WriteToBuf { + #[allow(clippy::result_unit_err)] + fn write(&self, buf: &mut [u8]) -> Result; +} + +macro_rules! impl_write_to_buf { + ($type:ident, $arg_type:expr) => { + impl WriteToBuf for $type { + fn write(&self, buf: &mut [u8]) -> Result { + TagLenValue::::new($arg_type, &self.to_ne_bytes()).write(buf) + } + } + }; +} + +impl_write_to_buf!(i8, ArgType::I8); +impl_write_to_buf!(i16, ArgType::I16); +impl_write_to_buf!(i32, ArgType::I32); +impl_write_to_buf!(i64, ArgType::I64); +impl_write_to_buf!(i128, ArgType::I128); +impl_write_to_buf!(isize, ArgType::Isize); + +impl_write_to_buf!(u8, ArgType::U8); +impl_write_to_buf!(u16, ArgType::U16); +impl_write_to_buf!(u32, ArgType::U32); +impl_write_to_buf!(u64, ArgType::U64); +impl_write_to_buf!(u128, ArgType::U128); +impl_write_to_buf!(usize, ArgType::Usize); + +impl_write_to_buf!(f32, ArgType::F32); +impl_write_to_buf!(f64, ArgType::F64); + +impl WriteToBuf for str { + fn write(&self, buf: &mut [u8]) -> Result { + TagLenValue::::new(ArgType::Str, self.as_bytes()).write(buf) + } +} + +#[allow(clippy::result_unit_err)] +#[doc(hidden)] +#[inline(always)] +pub fn write_record_header( + buf: &mut [u8], + target: &str, + level: Level, + module: &str, + file: &str, + line: u32, + num_args: usize, +) -> Result { + let mut size = 0; + for attr in [ + TagLenValue::::new(RecordField::Target, target.as_bytes()), + TagLenValue::::new(RecordField::Level, &(level as usize).to_ne_bytes()), + TagLenValue::::new(RecordField::Module, module.as_bytes()), + TagLenValue::::new(RecordField::File, file.as_bytes()), + TagLenValue::::new(RecordField::Line, &line.to_ne_bytes()), + TagLenValue::::new(RecordField::NumArgs, &num_args.to_ne_bytes()), + ] { + size += attr.write(&mut buf[size..])?; + } + + Ok(size) +} + +#[allow(clippy::result_unit_err)] +#[doc(hidden)] +pub fn write_record_message(buf: &mut [u8], msg: &str) -> Result { + TagLenValue::::new(RecordField::Log, msg.as_bytes()).write(buf) +} diff --git a/aya-log-ebpf-macros/Cargo.toml b/aya-log-ebpf-macros/Cargo.toml new file mode 100644 index 00000000..9247eb82 --- /dev/null +++ b/aya-log-ebpf-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aya-log-ebpf-macros" +version = "0.1.0" +edition = "2018" + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" + +[lib] +proc-macro = true diff --git a/aya-log-ebpf-macros/src/expand.rs b/aya-log-ebpf-macros/src/expand.rs new file mode 100644 index 00000000..b1279b12 --- /dev/null +++ b/aya-log-ebpf-macros/src/expand.rs @@ -0,0 +1,189 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Error, Expr, LitStr, Result, Token, +}; + +pub(crate) struct LogArgs { + pub(crate) ctx: Expr, + pub(crate) target: Option, + pub(crate) level: Option, + pub(crate) format_string: LitStr, + pub(crate) formatting_args: Option>, +} + +mod kw { + syn::custom_keyword!(target); +} + +impl Parse for LogArgs { + fn parse(input: ParseStream) -> Result { + let ctx: Expr = input.parse()?; + input.parse::()?; + + // Parse `target: &str`, which is an optional argument. + let target: Option = if input.peek(kw::target) { + input.parse::()?; + input.parse::()?; + let t: Expr = input.parse()?; + input.parse::()?; + Some(t) + } else { + None + }; + + // Check whether the next token is `format_string: &str` (which i + // always provided) or `level` (which is an optional expression). + // If `level` is provided, it comes before `format_string`. + let (level, format_string): (Option, LitStr) = if input.peek(LitStr) { + // Only `format_string` is provided. + (None, input.parse()?) + } else { + // Both `level` and `format_string` are provided. + let level: Expr = input.parse()?; + input.parse::()?; + let format_string: LitStr = input.parse()?; + (Some(level), format_string) + }; + + // Parse variadic arguments. + let formatting_args: Option> = if input.is_empty() { + None + } else { + input.parse::()?; + Some(Punctuated::parse_terminated(input)?) + }; + + Ok(Self { + ctx, + target, + level, + format_string, + formatting_args, + }) + } +} + +pub(crate) fn log(args: LogArgs, level: Option) -> Result { + let ctx = args.ctx; + let target = match args.target { + Some(t) => quote! { #t }, + None => quote! { module_path!() }, + }; + let lvl: TokenStream = if let Some(l) = level { + l + } else if let Some(l) = args.level { + quote! { #l } + } else { + return Err(Error::new( + args.format_string.span(), + "missing `level` argument: try passing an `aya_log_ebpf::Level` value", + )); + }; + let format_string = args.format_string; + + let (num_args, write_args) = match args.formatting_args { + Some(formatting_args) => { + let formatting_exprs = formatting_args.iter(); + let num_args = formatting_exprs.len(); + + let write_args = quote! {{ + use ::aya_log_ebpf::WriteToBuf; + Ok::<_, ()>(record_len) #( .and_then(|record_len| { + if record_len >= buf.buf.len() { + return Err(()); + } + { #formatting_exprs }.write(&mut buf.buf[record_len..]).map(|len| record_len + len) + }) )* + }}; + + (num_args, write_args) + } + None => (0, quote! {}), + }; + + // The way of writing to the perf buffer is different depending on whether + // we have variadic arguments or not. + let write_to_perf_buffer = if num_args > 0 { + // Writing with variadic arguments. + quote! { + if let Ok(record_len) = #write_args { + unsafe { ::aya_log_ebpf::AYA_LOGS.output( + #ctx, + &buf.buf[..record_len], 0 + )} + } + } + } else { + // Writing with no variadic arguments. + quote! { + unsafe { ::aya_log_ebpf::AYA_LOGS.output( + #ctx, + &buf.buf[..record_len], 0 + )} + } + }; + + Ok(quote! { + { + if let Some(buf_ptr) = unsafe { ::aya_log_ebpf::AYA_LOG_BUF.get_ptr_mut(0) } { + let buf = unsafe { &mut *buf_ptr }; + if let Ok(header_len) = ::aya_log_ebpf::write_record_header( + &mut buf.buf, + #target, + #lvl, + module_path!(), + file!(), + line!(), + #num_args, + ) { + if let Ok(message_len) = ::aya_log_ebpf::write_record_message( + &mut buf.buf[header_len..], + #format_string, + ) { + let record_len = header_len + message_len; + + #write_to_perf_buffer + } + } + } + } + }) +} + +pub(crate) fn error(args: LogArgs) -> Result { + log( + args, + Some(quote! { ::aya_log_ebpf::macro_support::Level::Error }), + ) +} + +pub(crate) fn warn(args: LogArgs) -> Result { + log( + args, + Some(quote! { ::aya_log_ebpf::macro_support::Level::Warn }), + ) +} + +pub(crate) fn info(args: LogArgs) -> Result { + log( + args, + Some(quote! { ::aya_log_ebpf::macro_support::Level::Info }), + ) +} + +pub(crate) fn debug(args: LogArgs) -> Result { + log( + args, + Some(quote! { ::aya_log_ebpf::macro_support::Level::Debug }), + ) +} + +pub(crate) fn trace(args: LogArgs) -> Result { + log( + args, + Some(quote! { ::aya_log_ebpf::macro_support::Level::Trace }), + ) +} diff --git a/aya-log-ebpf-macros/src/lib.rs b/aya-log-ebpf-macros/src/lib.rs new file mode 100644 index 00000000..264d93fb --- /dev/null +++ b/aya-log-ebpf-macros/src/lib.rs @@ -0,0 +1,52 @@ +use proc_macro::TokenStream; +use syn::parse_macro_input; + +mod expand; + +#[proc_macro] +pub fn log(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::log(args, None) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro] +pub fn error(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::error(args) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro] +pub fn warn(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::warn(args) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro] +pub fn info(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::info(args) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro] +pub fn debug(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::debug(args) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro] +pub fn trace(args: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as expand::LogArgs); + expand::trace(args) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/aya-log/Cargo.toml b/aya-log/Cargo.toml new file mode 100644 index 00000000..701f3adb --- /dev/null +++ b/aya-log/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "aya-log" +version = "0.1.11-dev.0" +description = "A logging library for eBPF programs." +keywords = ["ebpf", "bpf", "log", "logging"] +license = "MIT OR Apache-2.0" +authors = ["The Aya Contributors"] +repository = "https://github.com/aya-rs/aya-log" +readme = "README.md" +documentation = "https://docs.rs/aya-log" +edition = "2018" + +[dependencies] +aya = { path = "../aya", version = "0.11.0", features=["async_tokio"] } +aya-log-common = { path = "../aya-log-common", version = "0.1.11-dev.0", features=["userspace"] } +dyn-fmt = "0.3.0" +thiserror = "1" +log = "0.4" +bytes = "1.1" +tokio = { version = "1.2.0" } + +[dev-dependencies] +simplelog = "0.12" +testing_logger = "0.1.1" + +[lib] +path = "src/lib.rs" diff --git a/aya-log/README.md b/aya-log/README.md new file mode 100644 index 00000000..5e1245ca --- /dev/null +++ b/aya-log/README.md @@ -0,0 +1,73 @@ +# aya-log - a logging library for eBPF programs + +## Overview + +`aya-log` is a logging library for eBPF programs written using [aya]. Think of +it as the [log] crate for eBPF. + +## Installation + +### User space + +Add `aya-log` to `Cargo.toml`: + +```toml +[dependencies] +aya-log = { git = "https://github.com/aya-rs/aya-log", branch = "main" } +``` + +### eBPF side + +Add `aya-log-ebpf` to `Cargo.toml`: + +```toml +[dependencies] +aya-log-ebpf = { git = "https://github.com/aya-rs/aya-log", branch = "main" } +``` + +## Example + +Here's an example that uses `aya-log` in conjunction with the [simplelog] crate +to log eBPF messages to the terminal. + +### User space code + +```rust +use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; +use aya_log::BpfLogger; + +TermLogger::init( + LevelFilter::Debug, + ConfigBuilder::new() + .set_target_level(LevelFilter::Error) + .set_location_level(LevelFilter::Error) + .build(), + TerminalMode::Mixed, + ColorChoice::Auto, +) +.unwrap(); + +// Will log using the default logger, which is TermLogger in this case +BpfLogger::init(&mut bpf).unwrap(); +``` + +### eBPF code + +```rust +use aya_log_ebpf::info; + +fn try_xdp_firewall(ctx: XdpContext) -> Result { + if let Some(port) = tcp_dest_port(&ctx)? { + if block_port(port) { + info!(&ctx, "❌ blocked incoming connection on port: {}", port); + return Ok(XDP_DROP); + } + } + + Ok(XDP_PASS) +} +``` + +[aya]: https://github.com/aya-rs/aya +[log]: https://docs.rs/log +[simplelog]: https://docs.rs/simplelog diff --git a/aya-log/release.toml b/aya-log/release.toml new file mode 100644 index 00000000..bf6eaefc --- /dev/null +++ b/aya-log/release.toml @@ -0,0 +1 @@ +shared-version = true diff --git a/aya-log/src/lib.rs b/aya-log/src/lib.rs new file mode 100644 index 00000000..fb82e448 --- /dev/null +++ b/aya-log/src/lib.rs @@ -0,0 +1,381 @@ +//! A logging framework for eBPF programs. +//! +//! This is the user space side of the [Aya] logging framework. For the eBPF +//! side, see the `aya-log-ebpf` crate. +//! +//! `aya-log` provides the [BpfLogger] type, which reads log records created by +//! `aya-log-ebpf` and logs them using the [log] crate. Any logger that +//! implements the [Log] trait can be used with this crate. +//! +//! # Example: +//! +//! This example uses the [simplelog] crate to log messages to the terminal. +//! +//! ```no_run +//! # let mut bpf = aya::Bpf::load(&[]).unwrap(); +//! use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; +//! use aya_log::BpfLogger; +//! +//! // initialize simplelog::TermLogger as the default logger +//! TermLogger::init( +//! LevelFilter::Debug, +//! ConfigBuilder::new() +//! .set_target_level(LevelFilter::Error) +//! .set_location_level(LevelFilter::Error) +//! .build(), +//! TerminalMode::Mixed, +//! ColorChoice::Auto, +//! ) +//! .unwrap(); +//! +//! // start reading aya-log records and log them using the default logger +//! BpfLogger::init(&mut bpf).unwrap(); +//! ``` +//! +//! With the following eBPF code: +//! +//! ```ignore +//! # let ctx = (); +//! use aya_log_ebpf::{debug, error, info, trace, warn}; +//! +//! error!(&ctx, "this is an error message 🚨"); +//! warn!(&ctx, "this is a warning message ⚠ī¸"); +//! info!(&ctx, "this is an info message ℹī¸"); +//! debug!(&ctx, "this is a debug message ī¸đŸ"); +//! trace!(&ctx, "this is a trace message 🔍"); +//! ``` +//! Outputs: +//! +//! ```text +//! 21:58:55 [ERROR] xxx: [src/main.rs:35] this is an error message 🚨 +//! 21:58:55 [WARN] xxx: [src/main.rs:36] this is a warning message ⚠ī¸ +//! 21:58:55 [INFO] xxx: [src/main.rs:37] this is an info message ℹī¸ +//! 21:58:55 [DEBUG] (7) xxx: [src/main.rs:38] this is a debug message ī¸đŸ +//! 21:58:55 [TRACE] (7) xxx: [src/main.rs:39] this is a trace message 🔍 +//! ``` +//! +//! [Aya]: https://docs.rs/aya +//! [simplelog]: https://docs.rs/simplelog +//! [Log]: https://docs.rs/log/0.4.14/log/trait.Log.html +//! [log]: https://docs.rs/log +//! +use std::{convert::TryInto, io, mem, ptr, str, sync::Arc}; + +use aya_log_common::{ArgType, RecordField, LOG_BUF_CAPACITY, LOG_FIELDS}; +use bytes::BytesMut; +use dyn_fmt::AsStrFormatExt; +use log::{error, Level, Log, Record}; +use thiserror::Error; + +use aya::{ + maps::{ + perf::{AsyncPerfEventArray, PerfBufferError}, + MapError, + }, + util::online_cpus, + Bpf, Pod, +}; + +/// Log messages generated by `aya_log_ebpf` using the [log] crate. +/// +/// For more details see the [module level documentation](crate). +pub struct BpfLogger; + +impl BpfLogger { + /// Starts reading log records created with `aya-log-ebpf` and logs them + /// with the default logger. See [log::logger]. + pub fn init(bpf: &mut Bpf) -> Result { + BpfLogger::init_with_logger(bpf, DefaultLogger {}) + } + + /// Starts reading log records created with `aya-log-ebpf` and logs them + /// with the given logger. + pub fn init_with_logger( + bpf: &mut Bpf, + logger: T, + ) -> Result { + let logger = Arc::new(logger); + let mut logs: AsyncPerfEventArray<_> = bpf.map_mut("AYA_LOGS")?.try_into()?; + + for cpu_id in online_cpus().map_err(Error::InvalidOnlineCpu)? { + let mut buf = logs.open(cpu_id, None)?; + + let log = logger.clone(); + tokio::spawn(async move { + let mut buffers = (0..10) + .map(|_| BytesMut::with_capacity(LOG_BUF_CAPACITY)) + .collect::>(); + + loop { + let events = buf.read_events(&mut buffers).await.unwrap(); + + #[allow(clippy::needless_range_loop)] + for i in 0..events.read { + let buf = &mut buffers[i]; + log_buf(buf, &*log).unwrap(); + } + } + }); + } + + Ok(BpfLogger {}) + } +} + +#[derive(Copy, Clone, Debug)] +struct DefaultLogger; + +impl Log for DefaultLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + log::logger().enabled(metadata) + } + + fn log(&self, record: &Record) { + log::logger().log(record) + } + + fn flush(&self) { + log::logger().flush() + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("error opening log event array")] + MapError(#[from] MapError), + + #[error("error opening log buffer")] + PerfBufferError(#[from] PerfBufferError), + + #[error("invalid /sys/devices/system/cpu/online format")] + InvalidOnlineCpu(#[source] io::Error), +} + +fn log_buf(mut buf: &[u8], logger: &dyn Log) -> Result<(), ()> { + let mut target = None; + let mut level = Level::Trace; + let mut module = None; + let mut file = None; + let mut line = None; + let mut log = None; + let mut num_args = None; + + for _ in 0..LOG_FIELDS { + let (attr, rest) = unsafe { TagLenValue::<'_, RecordField>::try_read(buf)? }; + + match attr.tag { + RecordField::Target => { + target = Some(std::str::from_utf8(attr.value).map_err(|_| ())?); + } + RecordField::Level => { + level = unsafe { ptr::read_unaligned(attr.value.as_ptr() as *const _) } + } + RecordField::Module => { + module = Some(std::str::from_utf8(attr.value).map_err(|_| ())?); + } + RecordField::File => { + file = Some(std::str::from_utf8(attr.value).map_err(|_| ())?); + } + RecordField::Line => { + line = Some(u32::from_ne_bytes(attr.value.try_into().map_err(|_| ())?)); + } + RecordField::NumArgs => { + num_args = Some(usize::from_ne_bytes(attr.value.try_into().map_err(|_| ())?)); + } + RecordField::Log => { + log = Some(std::str::from_utf8(attr.value).map_err(|_| ())?); + } + } + + buf = rest; + } + + let log_msg = log.ok_or(())?; + let full_log_msg = match num_args { + Some(n) => { + let mut args: Vec = Vec::new(); + for _ in 0..n { + let (attr, rest) = unsafe { TagLenValue::<'_, ArgType>::try_read(buf)? }; + + match attr.tag { + ArgType::I8 => { + args.push( + i8::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::I16 => { + args.push( + i16::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::I32 => { + args.push( + i32::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::I64 => { + args.push( + i64::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::I128 => { + args.push( + i128::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::Isize => { + args.push( + isize::from_ne_bytes(attr.value.try_into().map_err(|_| ())?) + .to_string(), + ); + } + ArgType::U8 => { + args.push( + u8::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::U16 => { + args.push( + u16::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::U32 => { + args.push( + u32::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::U64 => { + args.push( + u64::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::U128 => { + args.push( + u128::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::Usize => { + args.push( + usize::from_ne_bytes(attr.value.try_into().map_err(|_| ())?) + .to_string(), + ); + } + ArgType::F32 => { + args.push( + f32::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::F64 => { + args.push( + f64::from_ne_bytes(attr.value.try_into().map_err(|_| ())?).to_string(), + ); + } + ArgType::Str => match str::from_utf8(attr.value) { + Ok(v) => args.push(v.to_string()), + Err(e) => error!("received invalid utf8 string: {}", e), + }, + } + + buf = rest; + } + + log_msg.format(&args) + } + None => log_msg.to_string(), + }; + + logger.log( + &Record::builder() + .args(format_args!("{}", full_log_msg)) + .target(target.ok_or(())?) + .level(level) + .module_path(module) + .file(file) + .line(line) + .build(), + ); + logger.flush(); + Ok(()) +} + +struct TagLenValue<'a, T: Pod> { + tag: T, + value: &'a [u8], +} + +impl<'a, T: Pod> TagLenValue<'a, T> { + unsafe fn try_read(mut buf: &'a [u8]) -> Result<(TagLenValue<'a, T>, &'a [u8]), ()> { + if buf.len() < mem::size_of::() + mem::size_of::() { + return Err(()); + } + + let tag = ptr::read_unaligned(buf.as_ptr() as *const T); + buf = &buf[mem::size_of::()..]; + + let len = usize::from_ne_bytes(buf[..mem::size_of::()].try_into().unwrap()); + buf = &buf[mem::size_of::()..]; + + if buf.len() < len { + return Err(()); + } + + Ok(( + TagLenValue { + tag, + value: &buf[..len], + }, + &buf[len..], + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + use aya_log_common::{write_record_header, write_record_message, WriteToBuf}; + use log::logger; + use testing_logger; + + fn new_log(msg: &str, args: usize) -> Result<(usize, Vec), ()> { + let mut buf = vec![0; 8192]; + let mut len = write_record_header( + &mut buf, + "test", + aya_log_common::Level::Info, + "test", + "test.rs", + 123, + args, + )?; + len += write_record_message(&mut buf[len..], msg)?; + Ok((len, buf)) + } + + #[test] + fn test_str() { + testing_logger::setup(); + let (_, input) = new_log("test", 0).unwrap(); + let logger = logger(); + let _ = log_buf(&input, logger); + testing_logger::validate(|captured_logs| { + assert_eq!(captured_logs.len(), 1); + assert_eq!(captured_logs[0].body, "test"); + assert_eq!(captured_logs[0].level, Level::Info); + }); + } + + #[test] + fn test_str_with_args() { + testing_logger::setup(); + let (len, mut input) = new_log("hello {}", 1).unwrap(); + let name = "test"; + (*name).write(&mut input[len..]).unwrap(); + let logger = logger(); + let _ = log_buf(&input, logger); + testing_logger::validate(|captured_logs| { + assert_eq!(captured_logs.len(), 1); + assert_eq!(captured_logs[0].body, "hello test"); + assert_eq!(captured_logs[0].level, Level::Info); + }); + } +} diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs index 8e390681..e0e8d066 100644 --- a/aya/src/maps/mod.rs +++ b/aya/src/maps/mod.rs @@ -33,7 +33,7 @@ //! versa. Because of that, all map values must be plain old data and therefore //! implement the [Pod] trait. use std::{ - convert::TryFrom, + convert::{TryFrom, TryInto}, ffi::CString, fmt, io, marker::PhantomData, @@ -226,7 +226,7 @@ impl AsRawFd for MapFd { } #[derive(PartialEq, Eq, PartialOrd, Ord)] -struct RlimitSize(u64); +struct RlimitSize(usize); impl fmt::Display for RlimitSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.0 < 1024 { @@ -246,8 +246,9 @@ fn maybe_warn_rlimit() { let ret = unsafe { getrlimit(RLIMIT_MEMLOCK, limit.as_mut_ptr()) }; if ret == 0 { let limit = unsafe { limit.assume_init() }; - let limit: RlimitSize = RlimitSize(limit.rlim_cur); - if limit.0 == RLIM_INFINITY { + + let limit: RlimitSize = RlimitSize(limit.rlim_cur.try_into().unwrap()); + if limit.0 == RLIM_INFINITY.try_into().unwrap() { return; } warn!( diff --git a/bpf/.cargo/config.toml b/bpf/.cargo/config.toml new file mode 100644 index 00000000..5d7e5915 --- /dev/null +++ b/bpf/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "../target" +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] \ No newline at end of file diff --git a/bpf/Cargo.toml b/bpf/Cargo.toml deleted file mode 100644 index b7cf3e2c..00000000 --- a/bpf/Cargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[workspace] -members = ["aya-bpf", "aya-bpf-macros", "aya-bpf-bindings"] diff --git a/bpf/aya-bpf/Cargo.toml b/bpf/aya-bpf/Cargo.toml index c28551c2..03fedb59 100644 --- a/bpf/aya-bpf/Cargo.toml +++ b/bpf/aya-bpf/Cargo.toml @@ -6,5 +6,8 @@ edition = "2018" [dependencies] aya-bpf-cty = { path = "../aya-bpf-cty" } -aya-bpf-macros = { path = "../aya-bpf-macros" } +aya-bpf-macros = { path = "../../aya-bpf-macros" } aya-bpf-bindings = { path = "../aya-bpf-bindings" } + +[build-dependencies] +rustversion = "1.0" \ No newline at end of file diff --git a/bpf/aya-bpf/build.rs b/bpf/aya-bpf/build.rs index a8a2c261..8ae418f2 100644 --- a/bpf/aya-bpf/build.rs +++ b/bpf/aya-bpf/build.rs @@ -1,6 +1,7 @@ use std::env; fn main() { + check_rust_version(); println!("cargo:rerun-if-env-changed=CARGO_CFG_BPF_TARGET_ARCH"); if let Ok(arch) = env::var("CARGO_CFG_BPF_TARGET_ARCH") { println!("cargo:rustc-cfg=bpf_target_arch=\"{}\"", arch); @@ -10,3 +11,11 @@ fn main() { println!("cargo:rustc-cfg=bpf_target_arch=\"{}\"", arch); } } + +#[rustversion::nightly] +fn check_rust_version() { + println!("cargo:rustc-cfg=unstable"); +} + +#[rustversion::not(nightly)] +fn check_rust_version() {} diff --git a/bpf/aya-bpf/src/lib.rs b/bpf/aya-bpf/src/lib.rs index 8de7fc3b..d6855584 100644 --- a/bpf/aya-bpf/src/lib.rs +++ b/bpf/aya-bpf/src/lib.rs @@ -9,7 +9,7 @@ html_logo_url = "https://aya-rs.dev/assets/images/crabby.svg", html_favicon_url = "https://aya-rs.dev/assets/images/crabby.svg" )] -#![feature(never_type)] +#![cfg_attr(unstable, feature(never_type))] #![allow(clippy::missing_safety_doc)] #![no_std] diff --git a/bpf/aya-bpf/src/maps/program_array.rs b/bpf/aya-bpf/src/maps/program_array.rs index 62fa07d4..b7e54a6c 100644 --- a/bpf/aya-bpf/src/maps/program_array.rs +++ b/bpf/aya-bpf/src/maps/program_array.rs @@ -80,6 +80,30 @@ impl ProgramArray { /// /// On success, this function **does not return** into the original program. /// On failure, a negative error is returned, wrapped in `Err()`. + #[cfg(not(unstable))] + pub unsafe fn tail_call(&self, ctx: &C, index: u32) -> Result<(), c_long> { + let res = bpf_tail_call(ctx.as_ptr(), self.def.get() as *mut _, index); + if res != 0 { + Err(res) + } else { + unreachable_unchecked() + } + } + + /// Perform a tail call into a program indexed by this map. + /// + /// # Safety + /// + /// This function is inherently unsafe, since it causes control flow to jump into + /// another eBPF program. This can have side effects, such as drop methods not being + /// called. Note that tail calling into an eBPF program is not the same thing as + /// a function call -- control flow never returns to the caller. + /// + /// # Return Value + /// + /// On success, this function **does not return** into the original program. + /// On failure, a negative error is returned, wrapped in `Err()`. + #[cfg(unstable)] pub unsafe fn tail_call(&self, ctx: &C, index: u32) -> Result { let res = bpf_tail_call(ctx.as_ptr(), self.def.get() as *mut _, index); if res != 0 { diff --git a/bpf/aya-log-ebpf/Cargo.toml b/bpf/aya-log-ebpf/Cargo.toml new file mode 100644 index 00000000..ffbd9dca --- /dev/null +++ b/bpf/aya-log-ebpf/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aya-log-ebpf" +version = "0.1.0" +edition = "2018" + +[dependencies] +aya-bpf = { path = "../aya-bpf" } +aya-log-common = { path = "../../aya-log-common" } +aya-log-ebpf-macros = { path = "../../aya-log-ebpf-macros" } + +[lib] +path = "src/lib.rs" diff --git a/bpf/aya-log-ebpf/src/lib.rs b/bpf/aya-log-ebpf/src/lib.rs new file mode 100644 index 00000000..6faa949d --- /dev/null +++ b/bpf/aya-log-ebpf/src/lib.rs @@ -0,0 +1,29 @@ +#![no_std] +use aya_bpf::{ + macros::map, + maps::{PerCpuArray, PerfEventByteArray}, +}; +pub use aya_log_common::{ + write_record_header, write_record_message, Level, WriteToBuf, LOG_BUF_CAPACITY, +}; +pub use aya_log_ebpf_macros::{debug, error, info, log, trace, warn}; + +#[doc(hidden)] +#[repr(C)] +pub struct LogBuf { + pub buf: [u8; LOG_BUF_CAPACITY], +} + +#[doc(hidden)] +#[map] +pub static mut AYA_LOG_BUF: PerCpuArray = PerCpuArray::with_max_entries(1, 0); + +#[doc(hidden)] +#[map] +pub static mut AYA_LOGS: PerfEventByteArray = PerfEventByteArray::new(0); + +#[doc(hidden)] +pub mod macro_support { + pub use aya_log_common::{Level, LOG_BUF_CAPACITY}; + pub use aya_log_ebpf_macros::log; +} diff --git a/bpf/rust-toolchain.toml b/bpf/rust-toolchain.toml new file mode 100644 index 00000000..5d56faf9 --- /dev/null +++ b/bpf/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/bpf/rustfmt.toml b/bpf/rustfmt.toml deleted file mode 120000 index 39f97b04..00000000 --- a/bpf/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -../rustfmt.toml \ No newline at end of file diff --git a/netlify.toml b/netlify.toml index 0b8e1991..3ad62912 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,3 @@ [build] publish = "site" - command = "rustup toolchain install nightly && cargo xtask docs" + command = "rustup toolchain install nightly -c rust-src && cargo xtask docs" diff --git a/release.toml b/release.toml new file mode 100644 index 00000000..e5351ffa --- /dev/null +++ b/release.toml @@ -0,0 +1,6 @@ +pre-release-commit-message = "{crate_name}: release version {{version}}" +post-release-commit-message = "{crate_name}: start next development iteration {{next_version}}" +consolidate-pushes = true +consolidate-commits = true +dev-version = true +dev-version-ext = "dev.0" diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml index b2a526fa..8688d790 100644 --- a/test/integration-ebpf/Cargo.toml +++ b/test/integration-ebpf/Cargo.toml @@ -21,16 +21,4 @@ path = "src/pass.rs" [[bin]] name = "test" -path = "src/test.rs" - -[profile.dev] -panic = "abort" -opt-level = 2 -overflow-checks = false - -[profile.release] -panic = "abort" -debug = 2 - -[workspace] -members = [] +path = "src/test.rs" \ No newline at end of file diff --git a/xtask/src/docs/mod.rs b/xtask/src/docs/mod.rs index 0496ef63..8705cae7 100644 --- a/xtask/src/docs/mod.rs +++ b/xtask/src/docs/mod.rs @@ -8,67 +8,19 @@ use std::{fs, io, io::Write}; use indoc::indoc; pub fn docs() -> Result<(), anyhow::Error> { - let mut working_dir = PathBuf::from("."); - - let replace = Command::new("sed") - .current_dir(&working_dir) - .args(vec![ - "-i.bak", - "s/crabby.svg/crabby_dev.svg/", - "aya/src/lib.rs", - ]) - .status() - .expect("failed to replace logo"); - assert!(replace.success()); - - let mut header_path = PathBuf::from("."); - header_path.push("header.html"); + let current_dir = PathBuf::from("."); + let header_path = current_dir.join("header.html"); let mut header = fs::File::create(&header_path).expect("can't create header.html"); header .write_all(r#""#.as_bytes()) .expect("can't write header.html contents"); header.flush().expect("couldn't flush contents"); - let abs_header_path = fs::canonicalize(&header_path).unwrap(); - let args = vec!["+nightly", "doc", "--no-deps", "--all-features"]; - - let status = Command::new("cargo") - .current_dir(&working_dir) - .env( - "RUSTDOCFLAGS", - format!("--html-in-header {}", abs_header_path.to_str().unwrap()), - ) - .args(&args) - .status() - .expect("failed to build aya docs"); - assert!(status.success()); - - working_dir.push("bpf"); - - let replace = Command::new("sed") - .current_dir(&working_dir) - .args(vec![ - "-i.bak", - "s/crabby.svg/crabby_dev.svg/", - "aya-bpf/src/lib.rs", - ]) - .status() - .expect("failed to replace logo"); - assert!(replace.success()); - - let status = Command::new("cargo") - .current_dir(&working_dir) - .env( - "RUSTDOCFLAGS", - format!("--html-in-header {}", abs_header_path.to_str().unwrap()), - ) - .args(&args) - .status() - .expect("failed to build aya-bpf docs"); - assert!(status.success()); - copy_dir_all("./target/doc", "site/user")?; - copy_dir_all("./bpf/target/doc", "site/bpf")?; + build_docs(¤t_dir.join("aya"), &abs_header_path)?; + build_docs(¤t_dir.join("bpf/aya-bpf"), &abs_header_path)?; + copy_dir_all("./target/doc", "./site/user")?; + copy_dir_all("./target/bpfel-unknown-none/doc", "./site/bpf")?; let mut robots = fs::File::create("site/robots.txt").expect("can't create robots.txt"); robots @@ -98,14 +50,38 @@ pub fn docs() -> Result<(), anyhow::Error> { .as_bytes(), ) .expect("can't write index.html"); + Ok(()) +} + +fn build_docs(working_dir: &PathBuf, abs_header_path: &Path) -> Result<(), anyhow::Error> { + let replace = Command::new("sed") + .current_dir(&working_dir) + .args(vec!["-i.bak", "s/crabby.svg/crabby_dev.svg/", "src/lib.rs"]) + .status() + .expect("failed to replace logo"); + assert!(replace.success()); - fs::rename("aya/src/lib.rs.bak", "aya/src/lib.rs").unwrap(); - fs::rename("bpf/aya-bpf/src/lib.rs.bak", "bpf/aya-bpf/src/lib.rs").unwrap(); + let args = vec!["+nightly", "doc", "--no-deps", "--all-features"]; + let status = Command::new("cargo") + .current_dir(&working_dir) + .env( + "RUSTDOCFLAGS", + format!("--html-in-header {}", abs_header_path.to_str().unwrap()), + ) + .args(&args) + .status() + .expect("failed to build aya docs"); + assert!(status.success()); + fs::rename( + working_dir.join("src/lib.rs.bak"), + working_dir.join("src/lib.rs"), + ) + .unwrap(); Ok(()) } -fn copy_dir_all>(src: P, dst: P) -> io::Result<()> { +fn copy_dir_all, P2: AsRef>(src: P1, dst: P2) -> io::Result<()> { fs::create_dir_all(&dst)?; for entry in fs::read_dir(src)? { let entry = entry?;