--- a/cmds/fleet/Cargo.toml +++ b/cmds/fleet/Cargo.toml @@ -50,7 +50,7 @@ tracing-opentelemetry.workspace = true [features] -default = [] +default = ["indicatif"] # Not quite stable indicatif = [ "dep:tracing-indicatif", --- a/cmds/fleet/src/main.rs +++ b/cmds/fleet/src/main.rs @@ -120,7 +120,8 @@ let indicatif_layer = { use std::time::Duration; - IndicatifLayer::new().with_progress_style( + IndicatifLayer::new().with_max_progress_bars(10, Some(ProgressStyle::default_spinner())) + .with_progress_style( ProgressStyle::with_template( "{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}", ) --- a/crates/nix-eval/build.rs +++ b/crates/nix-eval/build.rs @@ -18,6 +18,7 @@ "nix-util", "nix-util-c", "nix-store", + "nix-store-c", "nix-expr", "nix-flake", "nix-fetchers", @@ -72,6 +73,12 @@ .include_paths .into_iter() .chain( + pkg_config::probe_library("nix-store-c") + .expect("nix-store-c") + .include_paths + .into_iter(), + ) + .chain( pkg_config::probe_library("nix-flake-c") .expect("nix-flake-c") .include_paths --- /dev/null +++ b/crates/nix-eval/src/drv.rs @@ -0,0 +1,150 @@ +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::CString; + +use anyhow::{Result, bail}; +use serde::Deserialize; + +use crate::nix_raw::{derivation_free, derivation_to_json, store_drv_from_store_path}; +use crate::{copy_nix_str, with_store_context}; + +fn store_dir() -> Result { + let mut out = String::new(); + with_store_context(|c, store, _| unsafe { + crate::nix_raw::store_get_storedir(c, store, Some(copy_nix_str), (&raw mut out).cast()) + })?; + Ok(out) +} + +fn to_absolute_store_path(store_dir: &str, path: &str) -> String { + if path.starts_with('/') { + path.to_owned() + } else { + format!("{store_dir}/{path}") + } +} + +pub struct Derivation(*mut crate::nix_raw::derivation); +unsafe impl Send for Derivation {} + +impl Derivation { + pub fn from_path(drv_path: &str) -> Result { + let path_c = CString::new(drv_path)?; + let store_path = with_store_context(|c, store, _| unsafe { + crate::nix_raw::store_parse_path(c, store, path_c.as_ptr()) + })?; + let drv = with_store_context(|c, store, _| unsafe { + store_drv_from_store_path(c, store, store_path) + }); + unsafe { crate::nix_raw::store_path_free(store_path) }; + let drv = drv?; + if drv.is_null() { + bail!("failed to read derivation from {drv_path}"); + } + Ok(Self(drv)) + } + + pub fn to_json_string(&self) -> Result { + let mut out = String::new(); + with_store_context(|c, _, _| unsafe { + derivation_to_json(c, self.0, Some(copy_nix_str), (&raw mut out).cast()) + })?; + Ok(out) + } + + pub fn parsed(&self) -> Result { + let s = self.to_json_string()?; + Ok(serde_json::from_str(&s)?) + } +} + +impl Drop for Derivation { + fn drop(&mut self) { + unsafe { derivation_free(self.0) }; + } +} + +#[derive(Debug, Deserialize)] +pub struct DrvParsed { + pub inputs: DrvInputs, + pub outputs: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct DrvInputs { + #[serde(default)] + pub srcs: Vec, + #[serde(default)] + pub drvs: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct DrvInputEntry { + pub outputs: Vec, +} + +#[derive(Debug)] +pub struct DrvGraph { + pub root: String, + pub nodes: HashMap, +} + +#[derive(Debug)] +pub struct DrvNode { + pub name: String, + pub input_drvs: HashMap>, + pub input_srcs: Vec, + pub outputs: Vec, +} + +impl DrvGraph { + pub fn resolve(drv_path: &str) -> Result { + let sd = store_dir()?; + let root = to_absolute_store_path(&sd, drv_path); + + let mut nodes = HashMap::new(); + let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + queue.push_back(root.clone()); + visited.insert(root.clone()); + + while let Some(path) = queue.pop_front() { + let drv = Derivation::from_path(&path)?; + let parsed = drv.parsed()?; + + let input_drvs: HashMap> = parsed + .inputs + .drvs + .into_iter() + .map(|(k, v)| (to_absolute_store_path(&sd, &k), v.outputs)) + .collect(); + + for dep_path in input_drvs.keys() { + if visited.insert(dep_path.clone()) { + queue.push_back(dep_path.clone()); + } + } + + nodes.insert( + path.clone(), + DrvNode { + name: extract_drv_name(&path), + input_drvs, + input_srcs: parsed.inputs.srcs, + outputs: parsed.outputs.into_keys().collect(), + }, + ); + } + + Ok(Self { root, nodes }) + } +} + +fn extract_drv_name(drv_path: &str) -> String { + drv_path + .rsplit('/') + .next() + .and_then(|f| f.strip_suffix(".drv")) + .and_then(|f| f.split_once('-').map(|(_, name)| name)) + .unwrap_or(drv_path) + .to_owned() +} --- a/crates/nix-eval/src/lib.rs +++ b/crates/nix-eval/src/lib.rs @@ -13,7 +13,7 @@ use std::mem::transmute; pub use anyhow::Result; -use tracing::{Instrument, info, instrument, warn}; +use tracing::{Span, instrument, warn}; use self::logging::{ErrorInfoBuilder, nix_logging_cxx}; use self::nix_cxx::set_fetcher_setting; @@ -26,8 +26,9 @@ clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW, err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build, eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting, - expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new, - flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference, + expr_eval_from_string, fetchers_settings, + fetchers_settings_free, fetchers_settings_new, flake_lock, flake_lock_flags, + flake_lock_flags_free, flake_lock_flags_new, flake_reference, flake_reference_and_fragment_from_string, flake_reference_parse_flags, flake_reference_parse_flags_free, flake_reference_parse_flags_new, flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free, @@ -43,6 +44,7 @@ }; // Contains macros helpers +pub mod drv; pub mod logging; #[doc(hidden)] pub mod macros; @@ -321,7 +323,7 @@ thread_local! { static THREAD_STATE: RefCell = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail")); } -fn with_default_context(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result { +pub(crate) fn with_default_context(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result { let global = &GLOBAL_STATE.state; let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0)); let mut ctx = NixContext(ctx); @@ -331,6 +333,20 @@ v } +/// Same as with_default_context, but also passes store... +/// Yep, this code is garbage and needs to be refactored. +pub(crate) fn with_store_context( + f: impl FnOnce(*mut c_context, *mut c_store, *mut c_eval_state) -> T, +) -> Result { + let global = &GLOBAL_STATE; + let (ctx, store, state) = + THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.store.0, global.state.0)); + let mut ctx = NixContext(ctx); + let v = ctx.run_in_context(|c| f(c, store, state)); + std::mem::forget(ctx); + v +} + pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> { with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ()) } @@ -423,7 +439,7 @@ } } -unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) { +pub(crate) unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) { let s = unsafe { slice::from_raw_parts(start.cast::(), n as usize) }; let s = std::str::from_utf8(s).expect("c string has invalid utf-8"); unsafe { *user_data.cast::() = s.to_owned() }; @@ -836,6 +852,7 @@ })?; Ok(out) } + #[instrument(name = "build", skip(self), fields(output))] pub fn build(&self, output: &str) -> Result { if !self.is_derivation() { bail!("expected derivation to build") @@ -853,11 +870,19 @@ } else { self.clone() }; + + let drv_path = v + .get_field("drvPath") + .context("getting drvPath")? + .to_string()?; + let graph = drv::DrvGraph::resolve(&drv_path)?; + let _guard = logging::register_build_graph(&Span::current(), &graph); + // to_string here blocks until the path is built let s = v.builtin_to_string()?; let rs = s.to_realised_string()?; - let drv_path = rs.as_str().to_owned(); - Ok(PathBuf::from(drv_path)) + let out_path = rs.as_str().to_owned(); + Ok(PathBuf::from(out_path)) } pub fn as_json(&self) -> Result { let to_json = Self::eval("builtins.toJSON")?; @@ -1102,11 +1127,19 @@ assert_eq!(test_result, "PREFIX_BODY_SUFFIX"); let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix")); assert_eq!(test_result, "TESTsuffix"); - - let nix_ctx = NixContext::new(); - let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?; - // nix_raw::store_get_fs_closure(1); + let drv_path = nix_go!(attrs.packages["x86_64-linux"]["fleet-install-secrets"].drvPath) + .to_string()?; + let graph = drv::DrvGraph::resolve(&drv_path)?; + eprintln!( + "fleet-install-secrets dependency graph: {} nodes", + graph.nodes.len() + ); + for (path, node) in &graph.nodes { + if !node.input_drvs.is_empty() { + eprintln!(" {} ({} deps)", node.name, node.input_drvs.len()); + } + } Ok(()) } --- a/crates/nix-eval/src/logging.rs +++ b/crates/nix-eval/src/logging.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::fmt::Arguments; use std::sync::{LazyLock, Mutex}; @@ -285,6 +285,135 @@ static NIX_SPAN_MAPPING: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); +struct DrvGraphEntry { + name: String, + parent: Option, + span: Option, + refcount: usize, +} + +static DRV_GRAPH: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +static ACTIVITY_TO_DRV: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +pub struct BuildGraphGuard { + paths: Vec, +} + +impl Drop for BuildGraphGuard { + fn drop(&mut self) { + let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned"); + for path in &self.paths { + if let Some(entry) = drv_graph.get_mut(path) { + entry.refcount -= 1; + if entry.refcount == 0 { + drv_graph.remove(path); + } + } + } + } +} + +pub fn register_build_graph(parent: &Span, graph: &crate::drv::DrvGraph) -> BuildGraphGuard { + let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned"); + let mut paths = Vec::new(); + + drv_graph + .entry(graph.root.clone()) + .and_modify(|e| e.refcount += 1) + .or_insert_with(|| DrvGraphEntry { + name: graph.nodes[&graph.root].name.clone(), + parent: None, + span: Some(parent.clone()), + refcount: 1, + }); + paths.push(graph.root.clone()); + + let mut queue = VecDeque::new(); + queue.push_back(graph.root.clone()); + + let mut visited = std::collections::HashSet::new(); + visited.insert(graph.root.clone()); + + while let Some(path) = queue.pop_front() { + let Some(node) = graph.nodes.get(&path) else { + continue; + }; + for dep_path in node.input_drvs.keys() { + if !visited.insert(dep_path.clone()) { + continue; + } + let Some(dep_node) = graph.nodes.get(dep_path) else { + continue; + }; + if let Some(entry) = drv_graph.get_mut(dep_path) { + entry.refcount += 1; + } else { + drv_graph.insert(dep_path.clone(), DrvGraphEntry { + name: dep_node.name.clone(), + parent: Some(path.clone()), + span: None, + refcount: 1, + }); + } + paths.push(dep_path.clone()); + queue.push_back(dep_path.clone()); + } + } + + BuildGraphGuard { paths } +} + +fn ensure_drv_span(drv_path: &str) -> Option { + let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned"); + + if let Some(span) = drv_graph.get(drv_path).and_then(|e| e.span.clone()) { + return Some(span); + } + + let mut chain = vec![]; + let mut current = drv_path.to_owned(); + loop { + let Some(entry) = drv_graph.get(¤t) else { + break; + }; + if entry.span.is_some() { + chain.push(current); + break; + } + chain.push(current.clone()); + match &entry.parent { + Some(p) => current = p.clone(), + None => break, + } + } + + if chain.is_empty() { + return None; + } + + for i in (0..chain.len()).rev() { + let path = &chain[i]; + if drv_graph.get(path).unwrap().span.is_some() { + continue; + } + let parent_span = chain + .get(i + 1) + .and_then(|p| drv_graph.get(p)) + .and_then(|e| e.span.clone()); + let name = drv_graph.get(path).unwrap().name.clone(); + let span = { + let _enter = parent_span.as_ref().map(|s| s.enter()); + info_span!(target: "nix::build", "building", drv = %name) + }; + drv_graph.get_mut(path).unwrap().span = Some(span); + } + + drv_graph.get(drv_path).and_then(|e| e.span.clone()) +} + #[derive(Debug)] enum FieldValue { Int(i32), @@ -306,57 +435,33 @@ self.fields.push(FieldValue::Str(v.to_string())); } fn emit(&mut self, parent: u64, s: &str) { - let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned"); - - let parent = mapping.get(&parent); + let graph_span = if matches!(self.typ, ActivityType::Build) { + self.fields.first().and_then(|f| match f { + FieldValue::Str(drv_path) => { + let clean = parse_path(drv_path); + let span = ensure_drv_span(clean); + if span.is_some() { + ACTIVITY_TO_DRV + .lock() + .expect("not poisoned") + .insert(self.activity_id, clean.to_owned()); + } + span + } + _ => None, + }) + } else { + None + }; - // let meta = spans.alloc_metadata( - // self.typ.name(), - // self.verbosity.into(), - // MetadataKind::Span, - // "nix activity start", - // None, - // None, - // None, - // self.typ.fields(), - // ); - // - // let mut fields = meta.fields().iter(); - // let span = if let Some(parent) = parent { - // let s = Span::new( - // meta, - // &match meta.fields().len() { - // 1 => meta.fields().value_set( - // &<[_; 1]>::try_from([( - // &fields.next().expect("has field"), - // Some(&format_args!("Test") as &dyn tracing::Value), - // )]) - // .expect("valid size"), - // ), - // _ => unreachable!(), - // }, - // ); - // s.follows_from(parent); - // s - // } else { - // Span::new_root( - // meta, - // &match meta.fields().len() { - // 1 => meta.fields().value_set( - // &<[_; 1]>::try_from([( - // &fields.next().expect("has field"), - // Some(&format_args!("Test") as &dyn tracing::Value), - // )]) - // .expect("valid size"), - // ), - // _ => unreachable!(), - // }, - // ) - // }; - // - // let id = span.id().expect("id created"); + let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned"); - let span = { + let span = if let Some(span) = graph_span { + #[cfg(feature = "indicatif")] + span.pb_start(); + span + } else { + let parent = mapping.get(&parent); let _in_parent = parent.map(|p| p.enter()); let level: Level = self.verbosity.into(); if level == Level::ERROR { @@ -380,7 +485,7 @@ let s = ansi_filter(s); #[cfg(feature = "indicatif")] { - span.pb_set_message(s); + span.pb_set_message(&s); } let _e = span.enter(); let level: Level = self.verbosity.into(); @@ -454,8 +559,15 @@ warn!(target: "nix::eval", "{v}") } fn emit_stop(v: u64) { - let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned"); - mapping.remove(&v); + { + let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned"); + mapping.remove(&v); + } + if let Some(drv_path) = ACTIVITY_TO_DRV.lock().expect("not poisoned").remove(&v) { + if let Some(entry) = DRV_GRAPH.lock().expect("not poisoned").get_mut(&drv_path) { + entry.span = None; + } + } } fn emit_log(lvl: u32, v: &[u8]) { let verbosity = Verbosity::from_int(lvl);