git.delta.rocks / jrsonnet / refs/commits / f0286d779205

difftreelog

feat derivation graph to spans

xykwwsssYaroslav Bolyukin2026-03-12parent: #d5b0f6a.patch.diff
in: trunk

6 files changed

modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- 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",
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- 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}",
 			)
modifiedcrates/nix-eval/build.rsdiffbeforeafterboth
--- 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
addedcrates/nix-eval/src/drv.rsdiffbeforeafterboth
--- /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<String> {
+	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<Self> {
+		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<String> {
+		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<DrvParsed> {
+		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<String, serde_json::Value>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DrvInputs {
+	#[serde(default)]
+	pub srcs: Vec<String>,
+	#[serde(default)]
+	pub drvs: HashMap<String, DrvInputEntry>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DrvInputEntry {
+	pub outputs: Vec<String>,
+}
+
+#[derive(Debug)]
+pub struct DrvGraph {
+	pub root: String,
+	pub nodes: HashMap<String, DrvNode>,
+}
+
+#[derive(Debug)]
+pub struct DrvNode {
+	pub name: String,
+	pub input_drvs: HashMap<String, Vec<String>>,
+	pub input_srcs: Vec<String>,
+	pub outputs: Vec<String>,
+}
+
+impl DrvGraph {
+	pub fn resolve(drv_path: &str) -> Result<Self> {
+		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<String, Vec<String>> = 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()
+}
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- 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<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));
 }
-fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {
+pub(crate) fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {
 	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<T>(
+	f: impl FnOnce(*mut c_context, *mut c_store, *mut c_eval_state) -> T,
+) -> Result<T> {
+	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::<u8>(), n as usize) };
 	let s = std::str::from_utf8(s).expect("c string has invalid utf-8");
 	unsafe { *user_data.cast::<String>() = s.to_owned() };
@@ -836,6 +852,7 @@
 		})?;
 		Ok(out)
 	}
+	#[instrument(name = "build", skip(self), fields(output))]
 	pub fn build(&self, output: &str) -> Result<PathBuf> {
 		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<T: DeserializeOwned>(&self) -> Result<T> {
 		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(())
 }
modifiedcrates/nix-eval/src/logging.rsdiffbeforeafterboth
1use std::collections::HashMap;1use std::collections::{HashMap, VecDeque};
2use std::fmt::Arguments;2use std::fmt::Arguments;
3use std::sync::{LazyLock, Mutex};3use std::sync::{LazyLock, Mutex};
44
285static NIX_SPAN_MAPPING: LazyLock<Mutex<HashMap<u64, Span>>> =285static NIX_SPAN_MAPPING: LazyLock<Mutex<HashMap<u64, Span>>> =
286 LazyLock::new(|| Mutex::new(HashMap::new()));286 LazyLock::new(|| Mutex::new(HashMap::new()));
287
288struct DrvGraphEntry {
289 name: String,
290 parent: Option<String>,
291 span: Option<Span>,
292 refcount: usize,
293}
294
295static DRV_GRAPH: LazyLock<Mutex<HashMap<String, DrvGraphEntry>>> =
296 LazyLock::new(|| Mutex::new(HashMap::new()));
297
298static ACTIVITY_TO_DRV: LazyLock<Mutex<HashMap<u64, String>>> =
299 LazyLock::new(|| Mutex::new(HashMap::new()));
300
301pub struct BuildGraphGuard {
302 paths: Vec<String>,
303}
304
305impl Drop for BuildGraphGuard {
306 fn drop(&mut self) {
307 let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned");
308 for path in &self.paths {
309 if let Some(entry) = drv_graph.get_mut(path) {
310 entry.refcount -= 1;
311 if entry.refcount == 0 {
312 drv_graph.remove(path);
313 }
314 }
315 }
316 }
317}
318
319pub fn register_build_graph(parent: &Span, graph: &crate::drv::DrvGraph) -> BuildGraphGuard {
320 let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned");
321 let mut paths = Vec::new();
322
323 drv_graph
324 .entry(graph.root.clone())
325 .and_modify(|e| e.refcount += 1)
326 .or_insert_with(|| DrvGraphEntry {
327 name: graph.nodes[&graph.root].name.clone(),
328 parent: None,
329 span: Some(parent.clone()),
330 refcount: 1,
331 });
332 paths.push(graph.root.clone());
333
334 let mut queue = VecDeque::new();
335 queue.push_back(graph.root.clone());
336
337 let mut visited = std::collections::HashSet::new();
338 visited.insert(graph.root.clone());
339
340 while let Some(path) = queue.pop_front() {
341 let Some(node) = graph.nodes.get(&path) else {
342 continue;
343 };
344 for dep_path in node.input_drvs.keys() {
345 if !visited.insert(dep_path.clone()) {
346 continue;
347 }
348 let Some(dep_node) = graph.nodes.get(dep_path) else {
349 continue;
350 };
351 if let Some(entry) = drv_graph.get_mut(dep_path) {
352 entry.refcount += 1;
353 } else {
354 drv_graph.insert(dep_path.clone(), DrvGraphEntry {
355 name: dep_node.name.clone(),
356 parent: Some(path.clone()),
357 span: None,
358 refcount: 1,
359 });
360 }
361 paths.push(dep_path.clone());
362 queue.push_back(dep_path.clone());
363 }
364 }
365
366 BuildGraphGuard { paths }
367}
368
369fn ensure_drv_span(drv_path: &str) -> Option<Span> {
370 let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned");
371
372 if let Some(span) = drv_graph.get(drv_path).and_then(|e| e.span.clone()) {
373 return Some(span);
374 }
375
376 let mut chain = vec![];
377 let mut current = drv_path.to_owned();
378 loop {
379 let Some(entry) = drv_graph.get(&current) else {
380 break;
381 };
382 if entry.span.is_some() {
383 chain.push(current);
384 break;
385 }
386 chain.push(current.clone());
387 match &entry.parent {
388 Some(p) => current = p.clone(),
389 None => break,
390 }
391 }
392
393 if chain.is_empty() {
394 return None;
395 }
396
397 for i in (0..chain.len()).rev() {
398 let path = &chain[i];
399 if drv_graph.get(path).unwrap().span.is_some() {
400 continue;
401 }
402 let parent_span = chain
403 .get(i + 1)
404 .and_then(|p| drv_graph.get(p))
405 .and_then(|e| e.span.clone());
406 let name = drv_graph.get(path).unwrap().name.clone();
407 let span = {
408 let _enter = parent_span.as_ref().map(|s| s.enter());
409 info_span!(target: "nix::build", "building", drv = %name)
410 };
411 drv_graph.get_mut(path).unwrap().span = Some(span);
412 }
413
414 drv_graph.get(drv_path).and_then(|e| e.span.clone())
415}
287416
288#[derive(Debug)]417#[derive(Debug)]
289enum FieldValue {418enum FieldValue {
306 self.fields.push(FieldValue::Str(v.to_string()));435 self.fields.push(FieldValue::Str(v.to_string()));
307 }436 }
308 fn emit(&mut self, parent: u64, s: &str) {437 fn emit(&mut self, parent: u64, s: &str) {
438 let graph_span = if matches!(self.typ, ActivityType::Build) {
439 self.fields.first().and_then(|f| match f {
440 FieldValue::Str(drv_path) => {
441 let clean = parse_path(drv_path);
442 let span = ensure_drv_span(clean);
443 if span.is_some() {
444 ACTIVITY_TO_DRV
445 .lock()
446 .expect("not poisoned")
447 .insert(self.activity_id, clean.to_owned());
448 }
449 span
450 }
451 _ => None,
452 })
453 } else {
454 None
455 };
456
309 let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");457 let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");
310458
459 let span = if let Some(span) = graph_span {
460 #[cfg(feature = "indicatif")]
461 span.pb_start();
462 span
463 } else {
311 let parent = mapping.get(&parent);464 let parent = mapping.get(&parent);
312
313 // let meta = spans.alloc_metadata(
314 // self.typ.name(),
315 // self.verbosity.into(),
316 // MetadataKind::Span,
317 // "nix activity start",
318 // None,
319 // None,
320 // None,
321 // self.typ.fields(),
322 // );
323 //
324 // let mut fields = meta.fields().iter();
325 // let span = if let Some(parent) = parent {
326 // let s = Span::new(
327 // meta,
328 // &match meta.fields().len() {
329 // 1 => meta.fields().value_set(
330 // &<[_; 1]>::try_from([(
331 // &fields.next().expect("has field"),
332 // Some(&format_args!("Test") as &dyn tracing::Value),
333 // )])
334 // .expect("valid size"),
335 // ),
336 // _ => unreachable!(),
337 // },
338 // );
339 // s.follows_from(parent);
340 // s
341 // } else {
342 // Span::new_root(
343 // meta,
344 // &match meta.fields().len() {
345 // 1 => meta.fields().value_set(
346 // &<[_; 1]>::try_from([(
347 // &fields.next().expect("has field"),
348 // Some(&format_args!("Test") as &dyn tracing::Value),
349 // )])
350 // .expect("valid size"),
351 // ),
352 // _ => unreachable!(),
353 // },
354 // )
355 // };
356 //
357 // let id = span.id().expect("id created");
358
359 let span = {
360 let _in_parent = parent.map(|p| p.enter());465 let _in_parent = parent.map(|p| p.enter());
361 let level: Level = self.verbosity.into();466 let level: Level = self.verbosity.into();
362 if level == Level::ERROR {467 if level == Level::ERROR {
375 self.typ480 self.typ
376 .format(&self.fields, s, |v| trace_span!("action", v))481 .format(&self.fields, s, |v| trace_span!("action", v))
377 }482 }
378 };483 };
379 if !s.trim().is_empty() {484 if !s.trim().is_empty() {
380 let s = ansi_filter(s);485 let s = ansi_filter(s);
381 #[cfg(feature = "indicatif")]486 #[cfg(feature = "indicatif")]
382 {487 {
383 span.pb_set_message(s);488 span.pb_set_message(&s);
384 }489 }
385 let _e = span.enter();490 let _e = span.enter();
386 let level: Level = self.verbosity.into();491 let level: Level = self.verbosity.into();
454 warn!(target: "nix::eval", "{v}")559 warn!(target: "nix::eval", "{v}")
455}560}
456fn emit_stop(v: u64) {561fn emit_stop(v: u64) {
562 {
457 let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");563 let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");
458 mapping.remove(&v);564 mapping.remove(&v);
459}565 }
566 if let Some(drv_path) = ACTIVITY_TO_DRV.lock().expect("not poisoned").remove(&v) {
567 if let Some(entry) = DRV_GRAPH.lock().expect("not poisoned").get_mut(&drv_path) {
568 entry.span = None;
569 }
570 }
571}
460fn emit_log(lvl: u32, v: &[u8]) {572fn emit_log(lvl: u32, v: &[u8]) {
461 let verbosity = Verbosity::from_int(lvl);573 let verbosity = Verbosity::from_int(lvl);