diff --git a/bindgen-gcc.sh b/bindgen-gcc.sh index 923a278a..cdb71ebf 100644 --- a/bindgen-gcc.sh +++ b/bindgen-gcc.sh @@ -7,12 +7,10 @@ echo "Extending BINDGEN_EXTRA_CLANG_ARGS with system include paths..." 2>&1 BINDGEN_EXTRA_CLANG_ARGS="${BINDGEN_EXTRA_CLANG_ARGS:-}" export BINDGEN_EXTRA_CLANG_ARGS include_paths=$( - echo | $NIX_CC_UNWRAPPED -v -E -x c - 2>&1 \ + LC_ALL=C $NIX_CC_UNWRAPPED -v -E -x c - &1 \ | awk '/#include <...> search starts here:/{flag=1;next} \ /End of search list./{flag=0} \ flag==1 {print $1}' ) -for path in $include_paths; do - echo " - $path" 2>&1 - BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS -I$path" -done +include_args=$(printf '%s\n' "$include_paths" | awk 'NF {printf " -I%s", $1; printf " - %s\n", $1 > "/dev/stderr"}') +BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS$include_args" diff --git a/flake.lock b/flake.lock index 4c3bca70..048c8aaf 100644 --- a/flake.lock +++ b/flake.lock @@ -77,11 +77,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1777988971, + "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", "type": "github" }, "original": { @@ -159,11 +159,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1777402229, - "narHash": "sha256-h6WzecM2+aBt0rrcvG5+8d11q+nZoDm60na/48NcJ6w=", + "lastModified": 1778096718, + "narHash": "sha256-RnGxdnwhrEpLIqkRnXeb9caVLsKOboOMjMifEz+RaOk=", "owner": "DeterminateSystems", "repo": "nix-src", - "rev": "5ab3bee6fa77196de319a0b7669def091fc82253", + "rev": "8fb8ad7b69f5715c66720106c8b3a6dd8d0a51d8", "type": "github" }, "original": { @@ -185,11 +185,11 @@ "treefmt": "treefmt" }, "locked": { - "lastModified": 1777363774, - "narHash": "sha256-huAKxsrlMBzo1YxuKWwndoGv4K7FTfZ7azZeWtw7iNA=", + "lastModified": 1778141945, + "narHash": "sha256-unLKBy8SaNxEXaqMmXDUZyZIzf3K1H46azxFJDMq1jU=", "owner": "90-008", "repo": "nix-cargo-integration", - "rev": "fa3881a64ad9a04f1434494fc46cbf48dd9bef11", + "rev": "aceca529e10a880594b11c31a0e98d70d4a9c975", "type": "github" }, "original": { @@ -230,11 +230,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1774748309, - "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", "type": "github" }, "original": { @@ -261,11 +261,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1777268161, - "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { @@ -283,11 +283,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1777988971, + "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", "type": "github" }, "original": { @@ -358,11 +358,11 @@ ] }, "locked": { - "lastModified": 1777346187, - "narHash": "sha256-oVxyGjpiIsrXhWTJVUOs38fZQkLjd0nZGOY9K7Kfot8=", + "lastModified": 1778123869, + "narHash": "sha256-hV9D8ET33kXjdoMXBT2bwM/j8WQM1SP/dVKZtjQKhAQ=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "146e7bf7569b8288f24d41d806b9f584f7cfd5b5", + "rev": "592e5dedf04f0eaff1ed0f01ce5db7407d9fc7be", "type": "github" }, "original": { diff --git a/nix-bindings-util/src/lib.rs b/nix-bindings-util/src/lib.rs index 2c70adea..30accb70 100644 --- a/nix-bindings-util/src/lib.rs +++ b/nix-bindings-util/src/lib.rs @@ -6,6 +6,7 @@ use std::{ffi::NulError, str::Utf8Error, string::FromUtf8Error}; use thiserror::Error; pub mod context; +pub mod logger; pub mod settings; #[macro_use] pub mod string_return; diff --git a/nix-bindings-util/src/logger.rs b/nix-bindings-util/src/logger.rs new file mode 100644 index 00000000..66a7d611 --- /dev/null +++ b/nix-bindings-util/src/logger.rs @@ -0,0 +1,382 @@ +//! Custom Nix logger. +//! +//! Replace Nix's global logger with a Rust callback-based one that +//! receives log messages, activities, and string-valued results from +//! anywhere in libnixutil, libnixstore, and libnixexpr. +//! +//! See the [`Logger`] trait and [`set_logger`]. + +use crate::raw_sys as raw; +use crate::{check_call, context, Result}; +use std::ffi::{c_char, c_void, CStr}; +use std::sync::Mutex; + +/// Verbosity level of a log message. +/// +/// Mirrors the C `nix_verbosity` enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Verbosity { + Error, + Warn, + Notice, + Info, + Talkative, + Chatty, + Debug, + Vomit, + /// A verbosity level not (yet) modelled by this crate. + Unknown(u32), +} + +impl Verbosity { + fn from_raw(v: raw::verbosity) -> Self { + match v { + raw::verbosity_NIX_LVL_ERROR => Verbosity::Error, + raw::verbosity_NIX_LVL_WARN => Verbosity::Warn, + raw::verbosity_NIX_LVL_NOTICE => Verbosity::Notice, + raw::verbosity_NIX_LVL_INFO => Verbosity::Info, + raw::verbosity_NIX_LVL_TALKATIVE => Verbosity::Talkative, + raw::verbosity_NIX_LVL_CHATTY => Verbosity::Chatty, + raw::verbosity_NIX_LVL_DEBUG => Verbosity::Debug, + raw::verbosity_NIX_LVL_VOMIT => Verbosity::Vomit, + other => Verbosity::Unknown(other), + } + } +} + +/// Identifier for a logger activity. `0` is the implicit root. +pub type ActivityId = u64; + +/// Type of an activity. +/// +/// Mirrors the C `nix_activity_type` enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ActivityType { + Unknown, + CopyPath, + FileTransfer, + Realise, + CopyPaths, + Builds, + Build, + OptimiseStore, + VerifyPaths, + Substitute, + QueryPathInfo, + PostBuildHook, + BuildWaiting, + FetchTree, + /// An activity type not (yet) modelled by this crate. + Other(u32), +} + +impl ActivityType { + fn from_raw(t: raw::activity_type) -> Self { + match t { + raw::activity_type_NIX_ACTIVITY_TYPE_UNKNOWN => ActivityType::Unknown, + raw::activity_type_NIX_ACTIVITY_TYPE_COPY_PATH => ActivityType::CopyPath, + raw::activity_type_NIX_ACTIVITY_TYPE_FILE_TRANSFER => ActivityType::FileTransfer, + raw::activity_type_NIX_ACTIVITY_TYPE_REALISE => ActivityType::Realise, + raw::activity_type_NIX_ACTIVITY_TYPE_COPY_PATHS => ActivityType::CopyPaths, + raw::activity_type_NIX_ACTIVITY_TYPE_BUILDS => ActivityType::Builds, + raw::activity_type_NIX_ACTIVITY_TYPE_BUILD => ActivityType::Build, + raw::activity_type_NIX_ACTIVITY_TYPE_OPTIMISE_STORE => ActivityType::OptimiseStore, + raw::activity_type_NIX_ACTIVITY_TYPE_VERIFY_PATHS => ActivityType::VerifyPaths, + raw::activity_type_NIX_ACTIVITY_TYPE_SUBSTITUTE => ActivityType::Substitute, + raw::activity_type_NIX_ACTIVITY_TYPE_QUERY_PATH_INFO => ActivityType::QueryPathInfo, + raw::activity_type_NIX_ACTIVITY_TYPE_POST_BUILD_HOOK => ActivityType::PostBuildHook, + raw::activity_type_NIX_ACTIVITY_TYPE_BUILD_WAITING => ActivityType::BuildWaiting, + raw::activity_type_NIX_ACTIVITY_TYPE_FETCH_TREE => ActivityType::FetchTree, + other => ActivityType::Other(other), + } + } +} + +/// Type of a result reported by an activity. +/// +/// Only string-valued result types are delivered to +/// [`Logger::result_string`]; other variants exist for forward +/// compatibility. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ResultType { + FileLinked, + BuildLogLine, + UntrustedPath, + CorruptedPath, + SetPhase, + Progress, + SetExpected, + PostBuildLogLine, + FetchStatus, + HashMismatch, + BuildResult, + /// A result type not (yet) modelled by this crate. + Other(u32), +} + +impl ResultType { + fn from_raw(t: raw::result_type) -> Self { + match t { + raw::result_type_NIX_RESULT_TYPE_FILE_LINKED => ResultType::FileLinked, + raw::result_type_NIX_RESULT_TYPE_BUILD_LOG_LINE => ResultType::BuildLogLine, + raw::result_type_NIX_RESULT_TYPE_UNTRUSTED_PATH => ResultType::UntrustedPath, + raw::result_type_NIX_RESULT_TYPE_CORRUPTED_PATH => ResultType::CorruptedPath, + raw::result_type_NIX_RESULT_TYPE_SET_PHASE => ResultType::SetPhase, + raw::result_type_NIX_RESULT_TYPE_PROGRESS => ResultType::Progress, + raw::result_type_NIX_RESULT_TYPE_SET_EXPECTED => ResultType::SetExpected, + raw::result_type_NIX_RESULT_TYPE_POST_BUILD_LOG_LINE => ResultType::PostBuildLogLine, + raw::result_type_NIX_RESULT_TYPE_FETCH_STATUS => ResultType::FetchStatus, + raw::result_type_NIX_RESULT_TYPE_HASH_MISMATCH => ResultType::HashMismatch, + raw::result_type_NIX_RESULT_TYPE_BUILD_RESULT => ResultType::BuildResult, + other => ResultType::Other(other), + } + } +} + +/// A logger that receives messages from Nix. +/// +/// Implementations must be [`Send`] and [`Sync`] because Nix may +/// invoke the callbacks from arbitrary threads (e.g. during parallel +/// builds). +/// +/// All methods have empty default implementations, so an +/// implementation only needs to override the events it cares about. +/// +/// # Panics +/// +/// Callback methods must not unwind across the FFI boundary. +/// Panics from any of these methods are caught and silently +/// discarded; if your implementation may panic, prefer to handle +/// it yourself. +pub trait Logger: Send + Sync { + /// An ordinary log message. + /// + /// Receives `builtins.trace` output, formatted warnings/errors, + /// and messages produced through the C++ `printError` / + /// `printInfo` / `debug` macros. + /// + /// `msg` is decoded with [`String::from_utf8_lossy`] to keep the + /// signature ergonomic. + fn log(&self, _level: Verbosity, _msg: &str) {} + + /// An activity (build, substitution, ...) has started. + /// + /// `parent_id` is `0` for top-level activities. + fn start_activity( + &self, + _activity_id: ActivityId, + _level: Verbosity, + _type_: ActivityType, + _description: &str, + _parent_id: ActivityId, + ) { + } + + /// An activity has stopped. + fn stop_activity(&self, _activity_id: ActivityId) {} + + /// An activity reported a string-valued result. + /// + /// Result types that carry non-string fields (such as + /// [`ResultType::Progress`]) are not delivered through this + /// method. + fn result_string(&self, _activity_id: ActivityId, _type_: ResultType, _msg: &str) {} +} + +/// Erased trait object passed across the FFI boundary as `userdata`. +type LoggerObj = Box; + +/// Serializes calls to [`set_logger`] from the Rust side. +/// +/// Note this does *not* protect against C++ code (or other clients of +/// the C API) replacing the global logger concurrently — it only +/// avoids racing with ourselves. +static SET_LOGGER_MUTEX: Mutex<()> = Mutex::new(()); + +unsafe extern "C" fn thunk_log(userdata: *mut c_void, level: raw::verbosity, msg: *const c_char) { + assert!(!userdata.is_null()); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let logger = &*(userdata as *const LoggerObj); + let msg_str = if msg.is_null() { + std::borrow::Cow::Borrowed("") + } else { + CStr::from_ptr(msg).to_string_lossy() + }; + logger.log(Verbosity::from_raw(level), msg_str.as_ref()); + })); +} + +unsafe extern "C" fn thunk_start_activity( + userdata: *mut c_void, + activity_id: raw::activity_id, + level: raw::verbosity, + type_: raw::activity_type, + s: *const c_char, + parent_id: raw::activity_id, +) { + assert!(!userdata.is_null()); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let logger = &*(userdata as *const LoggerObj); + let s_str = if s.is_null() { + std::borrow::Cow::Borrowed("") + } else { + CStr::from_ptr(s).to_string_lossy() + }; + logger.start_activity( + activity_id, + Verbosity::from_raw(level), + ActivityType::from_raw(type_), + s_str.as_ref(), + parent_id, + ); + })); +} + +unsafe extern "C" fn thunk_stop_activity(userdata: *mut c_void, activity_id: raw::activity_id) { + assert!(!userdata.is_null()); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let logger = &*(userdata as *const LoggerObj); + logger.stop_activity(activity_id); + })); +} + +unsafe extern "C" fn thunk_result_string( + userdata: *mut c_void, + activity_id: raw::activity_id, + type_: raw::result_type, + msg: *const c_char, +) { + assert!(!userdata.is_null()); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let logger = &*(userdata as *const LoggerObj); + let msg_str = if msg.is_null() { + std::borrow::Cow::Borrowed("") + } else { + CStr::from_ptr(msg).to_string_lossy() + }; + logger.result_string(activity_id, ResultType::from_raw(type_), msg_str.as_ref()); + })); +} + +unsafe extern "C" fn thunk_destroy(userdata: *mut c_void) { + if userdata.is_null() { + return; + } + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + drop(Box::from_raw(userdata as *mut LoggerObj)); + })); +} + +static VTABLE: raw::logger = raw::logger { + log: Some(thunk_log), + start_activity: Some(thunk_start_activity), + stop_activity: Some(thunk_stop_activity), + result_string: Some(thunk_result_string), + destroy: Some(thunk_destroy), +}; + +/// Replace Nix's global logger with `logger`. +/// +/// `logger` is moved into a heap allocation owned by Nix; it is +/// dropped when this function is called again (replacing the logger) +/// or at process shutdown. +/// +/// # Thread safety +/// +/// This function serializes concurrent Rust callers via an internal +/// mutex. It does **not** protect against C++ code mutating +/// `nix::logger` directly. Like [`crate::settings::set`], prefer to +/// install a logger during single-threaded initialization. +#[doc(alias = "nix_set_logger")] +pub fn set_logger(logger: L) -> Result<()> { + let _guard = SET_LOGGER_MUTEX.lock().unwrap(); + + let boxed: Box = Box::new(Box::new(logger)); + let userdata = Box::into_raw(boxed) as *mut c_void; + + let mut ctx = context::Context::new(); + let res = unsafe { check_call!(raw::set_logger(&mut ctx, &VTABLE, userdata)) }; + + if let Err(e) = res { + // The C side did not accept the logger, so it will not invoke + // the destroy callback. Reclaim the box ourselves to avoid + // leaking the user-supplied logger. + unsafe { + drop(Box::from_raw(userdata as *mut LoggerObj)); + } + return Err(e); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::Arc; + + #[ctor::ctor] + fn setup() { + crate::init().unwrap(); + } + + /// Logger that records when it's dropped, so tests can observe the + /// destroy callback firing on replacement. + struct DropFlag { + dropped: Arc, + log_count: Arc, + } + + impl Logger for DropFlag { + fn log(&self, _level: Verbosity, _msg: &str) { + self.log_count.fetch_add(1, Ordering::SeqCst); + } + } + + impl Drop for DropFlag { + fn drop(&mut self) { + self.dropped.store(true, Ordering::SeqCst); + } + } + + #[test] + fn set_and_replace_calls_destroy() { + let dropped = Arc::new(AtomicBool::new(false)); + let log_count = Arc::new(AtomicUsize::new(0)); + + set_logger(DropFlag { + dropped: dropped.clone(), + log_count: log_count.clone(), + }) + .unwrap(); + + assert!(!dropped.load(Ordering::SeqCst)); + + // Replace with a fresh logger; this should drop the previous one. + set_logger(DropFlag { + dropped: Arc::new(AtomicBool::new(false)), + log_count: Arc::new(AtomicUsize::new(0)), + }) + .unwrap(); + + assert!( + dropped.load(Ordering::SeqCst), + "previous logger should be dropped when the logger is replaced" + ); + } + + #[test] + fn verbosity_round_trips_known_values() { + assert_eq!( + Verbosity::from_raw(raw::verbosity_NIX_LVL_ERROR), + Verbosity::Error + ); + assert_eq!( + Verbosity::from_raw(raw::verbosity_NIX_LVL_VOMIT), + Verbosity::Vomit + ); + } +}