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
before · cmds/fleet/src/main.rs
1#![recursion_limit = "512"]23pub(crate) mod cmds;4// pub(crate) mod command;5pub(crate) mod extra_args;67use std::{env, ffi::OsString, process::ExitCode, sync::Arc};89use anyhow::{Result, bail};10use clap::{CommandFactory, Parser};11use cmds::{12	build_systems::{BuildSystems, Deploy},13	complete::Complete,14	info::Info,15	rollback::RollbackSingle,16	secrets::Secret,17	tf::Tf,18};19use fleet_base::{host::Config, opts::FleetOpts};20use futures::{TryStreamExt, stream::FuturesUnordered};21#[cfg(feature = "indicatif")]22use human_repr::HumanCount;23#[cfg(feature = "indicatif")]24use indicatif::{ProgressState, ProgressStyle};25use nix_eval::{26	gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,27};28use tracing::{Instrument, error, info, info_span};29#[cfg(feature = "indicatif")]30use tracing_indicatif::IndicatifLayer;31use tracing_subscriber::{EnvFilter, prelude::*};3233#[derive(Parser)]34struct Prefetch {}35impl Prefetch {36	async fn run(&self, config: &Config) -> Result<()> {37		let mut prefetch_dir = config.directory.to_path_buf();38		prefetch_dir.push("prefetch");39		if !prefetch_dir.is_dir() {40			info!("nothing to prefetch: no prefetch directory");41			return Ok(());42		}43		let tasks = FuturesUnordered::new();44		for entry in std::fs::read_dir(&prefetch_dir)? {45			tasks.push(async {46				let entry = entry?;47				if !entry.metadata()?.is_file() {48					bail!("only files should exist in prefetch directory");49				}50				let span = info_span!(51					"prefetching",52					name = entry.file_name().to_string_lossy().as_ref()53				);54				let mut path = OsString::new();55				path.push("file://");56				path.push(entry.path());5758				let mut status = config.local_host().cmd("nix").await?;59				status.args(&config.nix_args);60				status.arg("store").arg("prefetch-file").arg(path);61				status.run_nix_string().instrument(span).await?;62				Ok(())63			});64		}65		tasks.try_collect::<Vec<()>>().await?;66		Ok(())67	}68}6970#[derive(Parser)]71enum Opts {72	/// Build system closures73	BuildSystems(BuildSystems),74	/// Upload and switch system closures75	Deploy(Deploy),76	/// Rollback remote machine by redeploying old generation as the new one77	RollbackSingle(RollbackSingle),78	/// Secret management79	#[clap(subcommand)]80	Secret(Secret),81	/// Upload prefetch directory to the nix store82	Prefetch(Prefetch),83	/// Config parsing84	Info(Info),85	/// Command completions86	#[clap(hide(true))]87	Complete(Complete),88	/// Compile and evaluate terranix configuration89	Tf(Tf),90}9192#[derive(Parser)]93#[clap(version, author)]94struct RootOpts {95	#[clap(flatten)]96	fleet_opts: FleetOpts,97	#[clap(subcommand)]98	command: Opts,99}100101async fn run_command(config: &Config, opts: FleetOpts, command: Opts) -> Result<()> {102	match command {103		Opts::BuildSystems(c) => c.run(config, &opts).await?,104		Opts::Deploy(d) => d.run(config, &opts).await?,105		Opts::RollbackSingle(r) => r.run(config, &opts).await?,106		Opts::Secret(s) => s.run(config, &opts).await?,107		Opts::Info(i) => i.run(config).await?,108		Opts::Prefetch(p) => p.run(config).await?,109		Opts::Tf(t) => t.run(config).await?,110		// TODO: actually parse commands before starting the async runtime111		Opts::Complete(c) => {112			tokio::task::spawn_blocking(move || c.run(RootOpts::command())).await?113		}114	};115	Ok(())116}117118fn setup_logging() {119	#[cfg(feature = "indicatif")]120	let indicatif_layer = {121		use std::time::Duration;122123		IndicatifLayer::new().with_progress_style(124			ProgressStyle::with_template(125				"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",126			)127				.unwrap()128				.with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {129					let Some(len) = state.len() else {130						return;131					};132					let pos = state.pos();133					if pos > len {134						let _ = write!(writer, "{}", pos.human_count_bare());135					} else {136						let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());137					}138				})139				.with_key(140					"color_start",141					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {142						let elapsed = state.elapsed();143144						if elapsed > Duration::from_secs(60) {145							// Red146							let _ = write!(writer, "\x1b[{}m", 1 + 30);147						} else if elapsed > Duration::from_secs(30) {148							// Yellow149							let _ = write!(writer, "\x1b[{}m", 3 + 30);150						}151					},152				)153				.with_key(154					"color_end",155					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {156						if state.elapsed() > Duration::from_secs(30) {157							let _ = write!(writer, "\x1b[0m");158						}159					},160				),161		)162	};163164	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));165166	let reg = tracing_subscriber::registry().with({167		let sub = tracing_subscriber::fmt::layer()168			.without_time()169			.with_target(false);170		#[cfg(feature = "indicatif")]171		let sub = sub.with_writer(indicatif_layer.get_stderr_writer());172		sub.with_filter(filter) // .without,173	});174175	if env::var_os("FLEET_OTEL").is_some() {}176177	// #[cfg(feature = "indicatif")]178	#[cfg(feature = "indicatif")]179	let reg = reg.with(indicatif_layer);180	reg.init();181}182183fn main() -> ExitCode {184	let opts = RootOpts::parse();185	if let Opts::Complete(c) = &opts.command {186		c.run(RootOpts::command());187		return ExitCode::SUCCESS;188	}189190	setup_logging();191192	init_libraries();193194	let runtime = tokio::runtime::Builder::new_multi_thread()195		.enable_all()196		.on_thread_start(|| {197			gc_register_my_thread();198		})199		.on_thread_stop(|| {200			gc_unregister_my_thread();201		})202		.build()203		.expect("failed to build runtime");204	let runtime = Arc::new(runtime);205206	init_tokio_for_nix(runtime.clone());207208	runtime.block_on(async {209		tokio::task::spawn(async move {210			if let Err(e) = main_real(opts).await {211				error!("{e:#}");212				ExitCode::FAILURE213			} else {214				ExitCode::SUCCESS215			}216		})217		.await218		.expect("primary task panicked")219	})220	// async_main(opts)221}222223async fn main_real(opts: RootOpts) -> Result<()> {224	let nix_args = std::env::var_os("NIX_ARGS")225		.map(|a| extra_args::parse_os(&a))226		.transpose()?227		.unwrap_or_default();228	let config = opts.fleet_opts.build(229		nix_args,230		matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),231	)?;232233	match run_command(&config, opts.fleet_opts, opts.command).await {234		Ok(()) => {235			config.save()?;236			Ok(())237		}238		Err(e) => {239			let _ = config.save();240			Err(e)241		}242	}243}244245#[cfg(test)]246mod tests {247	use super::*;248249	#[test]250	fn verify_command() {251		use clap::CommandFactory;252		RootOpts::command().debug_assert();253	}254}
after · cmds/fleet/src/main.rs
1#![recursion_limit = "512"]23pub(crate) mod cmds;4// pub(crate) mod command;5pub(crate) mod extra_args;67use std::{env, ffi::OsString, process::ExitCode, sync::Arc};89use anyhow::{Result, bail};10use clap::{CommandFactory, Parser};11use cmds::{12	build_systems::{BuildSystems, Deploy},13	complete::Complete,14	info::Info,15	rollback::RollbackSingle,16	secrets::Secret,17	tf::Tf,18};19use fleet_base::{host::Config, opts::FleetOpts};20use futures::{TryStreamExt, stream::FuturesUnordered};21#[cfg(feature = "indicatif")]22use human_repr::HumanCount;23#[cfg(feature = "indicatif")]24use indicatif::{ProgressState, ProgressStyle};25use nix_eval::{26	gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,27};28use tracing::{Instrument, error, info, info_span};29#[cfg(feature = "indicatif")]30use tracing_indicatif::IndicatifLayer;31use tracing_subscriber::{EnvFilter, prelude::*};3233#[derive(Parser)]34struct Prefetch {}35impl Prefetch {36	async fn run(&self, config: &Config) -> Result<()> {37		let mut prefetch_dir = config.directory.to_path_buf();38		prefetch_dir.push("prefetch");39		if !prefetch_dir.is_dir() {40			info!("nothing to prefetch: no prefetch directory");41			return Ok(());42		}43		let tasks = FuturesUnordered::new();44		for entry in std::fs::read_dir(&prefetch_dir)? {45			tasks.push(async {46				let entry = entry?;47				if !entry.metadata()?.is_file() {48					bail!("only files should exist in prefetch directory");49				}50				let span = info_span!(51					"prefetching",52					name = entry.file_name().to_string_lossy().as_ref()53				);54				let mut path = OsString::new();55				path.push("file://");56				path.push(entry.path());5758				let mut status = config.local_host().cmd("nix").await?;59				status.args(&config.nix_args);60				status.arg("store").arg("prefetch-file").arg(path);61				status.run_nix_string().instrument(span).await?;62				Ok(())63			});64		}65		tasks.try_collect::<Vec<()>>().await?;66		Ok(())67	}68}6970#[derive(Parser)]71enum Opts {72	/// Build system closures73	BuildSystems(BuildSystems),74	/// Upload and switch system closures75	Deploy(Deploy),76	/// Rollback remote machine by redeploying old generation as the new one77	RollbackSingle(RollbackSingle),78	/// Secret management79	#[clap(subcommand)]80	Secret(Secret),81	/// Upload prefetch directory to the nix store82	Prefetch(Prefetch),83	/// Config parsing84	Info(Info),85	/// Command completions86	#[clap(hide(true))]87	Complete(Complete),88	/// Compile and evaluate terranix configuration89	Tf(Tf),90}9192#[derive(Parser)]93#[clap(version, author)]94struct RootOpts {95	#[clap(flatten)]96	fleet_opts: FleetOpts,97	#[clap(subcommand)]98	command: Opts,99}100101async fn run_command(config: &Config, opts: FleetOpts, command: Opts) -> Result<()> {102	match command {103		Opts::BuildSystems(c) => c.run(config, &opts).await?,104		Opts::Deploy(d) => d.run(config, &opts).await?,105		Opts::RollbackSingle(r) => r.run(config, &opts).await?,106		Opts::Secret(s) => s.run(config, &opts).await?,107		Opts::Info(i) => i.run(config).await?,108		Opts::Prefetch(p) => p.run(config).await?,109		Opts::Tf(t) => t.run(config).await?,110		// TODO: actually parse commands before starting the async runtime111		Opts::Complete(c) => {112			tokio::task::spawn_blocking(move || c.run(RootOpts::command())).await?113		}114	};115	Ok(())116}117118fn setup_logging() {119	#[cfg(feature = "indicatif")]120	let indicatif_layer = {121		use std::time::Duration;122123		IndicatifLayer::new().with_max_progress_bars(10, Some(ProgressStyle::default_spinner()))124			.with_progress_style(125			ProgressStyle::with_template(126				"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",127			)128				.unwrap()129				.with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {130					let Some(len) = state.len() else {131						return;132					};133					let pos = state.pos();134					if pos > len {135						let _ = write!(writer, "{}", pos.human_count_bare());136					} else {137						let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());138					}139				})140				.with_key(141					"color_start",142					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {143						let elapsed = state.elapsed();144145						if elapsed > Duration::from_secs(60) {146							// Red147							let _ = write!(writer, "\x1b[{}m", 1 + 30);148						} else if elapsed > Duration::from_secs(30) {149							// Yellow150							let _ = write!(writer, "\x1b[{}m", 3 + 30);151						}152					},153				)154				.with_key(155					"color_end",156					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {157						if state.elapsed() > Duration::from_secs(30) {158							let _ = write!(writer, "\x1b[0m");159						}160					},161				),162		)163	};164165	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));166167	let reg = tracing_subscriber::registry().with({168		let sub = tracing_subscriber::fmt::layer()169			.without_time()170			.with_target(false);171		#[cfg(feature = "indicatif")]172		let sub = sub.with_writer(indicatif_layer.get_stderr_writer());173		sub.with_filter(filter) // .without,174	});175176	if env::var_os("FLEET_OTEL").is_some() {}177178	// #[cfg(feature = "indicatif")]179	#[cfg(feature = "indicatif")]180	let reg = reg.with(indicatif_layer);181	reg.init();182}183184fn main() -> ExitCode {185	let opts = RootOpts::parse();186	if let Opts::Complete(c) = &opts.command {187		c.run(RootOpts::command());188		return ExitCode::SUCCESS;189	}190191	setup_logging();192193	init_libraries();194195	let runtime = tokio::runtime::Builder::new_multi_thread()196		.enable_all()197		.on_thread_start(|| {198			gc_register_my_thread();199		})200		.on_thread_stop(|| {201			gc_unregister_my_thread();202		})203		.build()204		.expect("failed to build runtime");205	let runtime = Arc::new(runtime);206207	init_tokio_for_nix(runtime.clone());208209	runtime.block_on(async {210		tokio::task::spawn(async move {211			if let Err(e) = main_real(opts).await {212				error!("{e:#}");213				ExitCode::FAILURE214			} else {215				ExitCode::SUCCESS216			}217		})218		.await219		.expect("primary task panicked")220	})221	// async_main(opts)222}223224async fn main_real(opts: RootOpts) -> Result<()> {225	let nix_args = std::env::var_os("NIX_ARGS")226		.map(|a| extra_args::parse_os(&a))227		.transpose()?228		.unwrap_or_default();229	let config = opts.fleet_opts.build(230		nix_args,231		matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),232	)?;233234	match run_command(&config, opts.fleet_opts, opts.command).await {235		Ok(()) => {236			config.save()?;237			Ok(())238		}239		Err(e) => {240			let _ = config.save();241			Err(e)242		}243	}244}245246#[cfg(test)]247mod tests {248	use super::*;249250	#[test]251	fn verify_command() {252		use clap::CommandFactory;253		RootOpts::command().debug_assert();254	}255}
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
--- 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<Mutex<HashMap<u64, Span>>> =
 	LazyLock::new(|| Mutex::new(HashMap::new()));
 
+struct DrvGraphEntry {
+	name: String,
+	parent: Option<String>,
+	span: Option<Span>,
+	refcount: usize,
+}
+
+static DRV_GRAPH: LazyLock<Mutex<HashMap<String, DrvGraphEntry>>> =
+	LazyLock::new(|| Mutex::new(HashMap::new()));
+
+static ACTIVITY_TO_DRV: LazyLock<Mutex<HashMap<u64, String>>> =
+	LazyLock::new(|| Mutex::new(HashMap::new()));
+
+pub struct BuildGraphGuard {
+	paths: Vec<String>,
+}
+
+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<Span> {
+	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(&current) 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);