git.delta.rocks / jrsonnet / refs/commits / 521a65806d80

difftreelog

refactor use nix bindings instead of REPL

lzxxowxyYaroslav Bolyukin2025-09-01parent: #757475f.patch.diff
in: trunk

33 files changed

modifiedCargo.lockdiffbeforeafterboth
before · Cargo.lock
392 packageslockfile v4
after · Cargo.lock
426 packageslockfile v4
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@
 nix-eval = { path = "./crates/nix-eval" }
 nixlike = { path = "./crates/nixlike" }
 
-age = { version = "0.11", features = ["ssh", "plugin"] }
+age = { version = "0.11", features = ["plugin", "ssh"] }
 anyhow = "1.0"
 clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
 clap_complete = "4.5"
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -43,7 +43,7 @@
 
 fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
 human-repr = { version = "1.1", optional = true }
-indicatif = { version = "0.17", optional = true }
+indicatif = { version = "0.18", optional = true }
 nom = "8.0.0"
 tracing-indicatif = { version = "0.3", optional = true }
 
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,13 +1,13 @@
 use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
-use anyhow::{Result, anyhow};
+use anyhow::Result;
 use clap::Parser;
 use fleet_base::{
 	deploy::{DeployAction, deploy_task, upload_task},
 	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
-use nix_eval::{NixBuildBatch, nix_go};
+use nix_eval::nix_go;
 use tokio::task::LocalSet;
 use tracing::{Instrument, error, field, info, info_span, warn};
 
@@ -32,17 +32,15 @@
 	config: Config,
 	hostname: String,
 	build_attr: &str,
-	batch: Option<NixBuildBatch>,
+	// batch: Option<NixBuildBatch>,
 ) -> Result<PathBuf> {
 	info!("building");
 	let host = config.host(&hostname).await?;
 	// let action = Action::from(self.subcommand.clone());
 	let nixos = host.nixos_config().await?;
 	let drv = nix_go!(nixos.system.build[{ build_attr }]);
-	let outputs = drv.build_maybe_batch(batch).await?;
-	let out_output = outputs
-		.get("out")
-		.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
+	// let outputs = drv.build_maybe_batch(batch).await?;
+	let out_output = drv.build("out").await?;
 
 	// We already have system profiles for backups.
 	if !host.local {
@@ -56,11 +54,11 @@
 					config.data().gc_root_prefix
 				),
 			)
-			.arg(out_output);
+			.arg(&out_output);
 		cmd.sudo().run_nix().await?;
 	}
 
-	Ok(out_output.clone())
+	Ok(out_output)
 }
 
 impl BuildSystems {
@@ -68,21 +66,20 @@
 		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
 		let set = LocalSet::new();
 		let build_attr = self.build_attr.clone();
-		let batch = (hosts.len() > 1).then(|| {
-			config
-				.nix_session
-				.new_build_batch("build-hosts".to_string())
-		});
+		// let batch = (hosts.len() > 1).then(|| {
+		// 	config
+		// 		.nix_session
+		// 		.new_build_batch("build-hosts".to_string())
+		// });
 		for host in hosts {
 			let config = config.clone();
 			let span = info_span!("build", host = field::display(&host.name));
 			let hostname = host.name;
 			let build_attr = build_attr.clone();
-			let batch = batch.clone();
+			// let batch = batch.clone();
 			set.spawn_local(
 				(async move {
-					let built = match build_task(config, hostname.clone(), &build_attr, batch).await
-					{
+					let built = match build_task(config, hostname.clone(), &build_attr).await {
 						Ok(path) => path,
 						Err(e) => {
 							error!("failed to deploy host: {}", e);
@@ -91,7 +88,7 @@
 					};
 					// TODO: Handle error
 					let mut out = current_dir().expect("cwd exists");
-					out.push(format!("built-{}", hostname));
+					out.push(format!("built-{hostname}"));
 
 					info!("linking iso image to {:?}", out);
 					if let Err(e) = symlink(built, out) {
@@ -101,7 +98,6 @@
 				.instrument(span),
 			);
 		}
-		drop(batch);
 		set.await;
 		Ok(())
 	}
@@ -111,17 +107,16 @@
 	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
 		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
 		let set = LocalSet::new();
-		let batch = (hosts.len() > 1).then(|| {
-			config
-				.nix_session
-				.new_build_batch("deploy-hosts".to_string())
-		});
+		// let batch = (hosts.len() > 1).then(|| {
+		// 	config
+		// 		.nix_session
+		// 		.new_build_batch("deploy-hosts".to_string())
+		// });
 		for host in hosts.into_iter() {
 			let config = config.clone();
 			let span = info_span!("deploy", host = field::display(&host.name));
 			let hostname = host.name.clone();
 			let opts = opts.clone();
-			let batch = batch.clone();
 			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
 				host.set_deploy_kind(deploy_kind);
 			};
@@ -131,15 +126,14 @@
 
 			set.spawn_local(
 				(async move {
-					let built =
-						match build_task(config.clone(), hostname.clone(), "toplevel", batch).await
-						{
-							Ok(path) => path,
-							Err(e) => {
-								error!("failed to build host system closure: {}", e);
-								return;
-							}
-						};
+					let built = match build_task(config.clone(), hostname.clone(), "toplevel").await
+					{
+						Ok(path) => path,
+						Err(e) => {
+							error!("failed to build host system closure: {:#}", e);
+							return;
+						}
+					};
 
 					let deploy_kind = match host.deploy_kind().await {
 						Ok(v) => v,
@@ -187,7 +181,6 @@
 				.instrument(span),
 			);
 		}
-		drop(batch);
 		set.await;
 		Ok(())
 	}
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -38,7 +38,8 @@
 				'host: for host in config.list_hosts().await? {
 					if !tagged.is_empty() {
 						let config = &config.config_field;
-						let tags: Vec<String> = nix_go_json!(config.hosts[{ host.name }].tags);
+						let host_name = &host.name;
+						let tags: Vec<String> = nix_go_json!(config.hosts[host_name].tags);
 						for tag in tagged {
 							if !tags.contains(tag) {
 								continue 'host;
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -2,6 +2,7 @@
 	collections::{BTreeMap, BTreeSet, HashSet},
 	io::{self, Read, Write, stdin, stdout},
 	path::PathBuf,
+	slice,
 };
 
 use age::Recipient;
@@ -14,7 +15,7 @@
 	opts::FleetOpts,
 };
 use fleet_shared::SecretData;
-use nix_eval::{NixBuildBatch, Value, nix_go, nix_go_json};
+use nix_eval::{NixType, Value, nix_go, nix_go_json};
 use owo_colors::OwoColorize;
 use serde::Deserialize;
 use tabled::{Table, Tabled};
@@ -159,7 +160,7 @@
 }
 
 #[allow(clippy::too_many_arguments)]
-#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]
+#[tracing::instrument(skip(config, secret, field, prefer_identities))]
 async fn maybe_regenerate_shared_secret(
 	secret_name: &str,
 	config: &Config,
@@ -168,7 +169,7 @@
 	expected_owners: &[String],
 	expected_generation_data: serde_json::Value,
 	prefer_identities: &[String],
-	batch: Option<NixBuildBatch>,
+	// batch: Option<NixBuildBatch>,
 ) -> Result<FleetSharedSecret> {
 	let original_set = secret.owners.clone();
 
@@ -206,12 +207,12 @@
 			field,
 			expected_owners.to_vec(),
 			expected_generation_data,
-			batch,
+			// batch,
 		)
 		.await?;
 		Ok(generated)
 	} else {
-		drop(batch);
+		// drop(batch);
 		let identity_holder = if !prefer_identities.is_empty() {
 			prefer_identities
 				.iter()
@@ -263,7 +264,7 @@
 	default_generator: Value,
 	expected_owners: &[String],
 	expected_generation_data: serde_json::Value,
-	batch: Option<NixBuildBatch>,
+	// batch: Option<NixBuildBatch>,
 ) -> Result<FleetSecret> {
 	let generator = nix_go!(secret.generator);
 	let on: Option<String> = nix_go_json!(default_generator.impureOn);
@@ -284,17 +285,16 @@
 		recipients.push(key);
 	}
 	let generators = nix_go!(mk_secret_generators(Obj { recipients }));
-	let pkgs_and_generators = nix_go!(on_pkgs + generators);
+	// FIXME: Apparently, // operator is slow in nix
+	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;
 
 	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
 
 	let generator = nix_go!(call_package(generator)(Obj {}));
 
-	let generator = generator.build_maybe_batch(batch).await?;
-	let generator = generator
-		.get("out")
-		.ok_or_else(|| anyhow!("missing generateImpure out"))?;
-	let generator = host.remote_derivation(generator).await?;
+	// let generator = generator.build_maybe_batch(batch).await?;
+	let generator = generator.build("out").await?;
+	let generator = host.remote_derivation(&generator).await?;
 
 	let out_parent = host.mktemp_dir().await?;
 	let out = format!("{out_parent}/out");
@@ -347,21 +347,21 @@
 	secret: Value,
 	expected_owners: &[String],
 	expected_generation_data: serde_json::Value,
-	batch: Option<NixBuildBatch>,
+	// batch: Option<NixBuildBatch>,
 ) -> Result<FleetSecret> {
 	let generator = nix_go!(secret.generator);
 	// Can't properly check on nix module system level
 	{
-		let gen_ty = generator.type_of().await?;
-		if gen_ty == "null" {
+		let gen_ty = generator.type_of()?;
+		if matches!(gen_ty, NixType::Null) {
 			bail!("secret has no generator defined, can't automatically generate it.");
 		}
-		if gen_ty == "set" {
-			if !generator.has_field("__functor").await? {
-				bail!("generator should be functor, got {gen_ty}");
+		if matches!(gen_ty, NixType::Attrs) {
+			if !generator.has_field("__functor")? {
+				bail!("generator should be functor, got {gen_ty:?}");
 			}
-		} else if gen_ty != "lambda" {
-			bail!("generator should be functor, got {gen_ty}");
+		} else if matches!(gen_ty, NixType::Function) {
+			bail!("generator should be functor, got {gen_ty:?}");
 		}
 	}
 	let nixpkgs = &config.nixpkgs;
@@ -378,7 +378,7 @@
 	let generators = nix_go!(default_mk_secret_generators(Obj {
 		recipients: <Vec<String>>::new(),
 	}));
-	let pkgs_and_generators = nix_go!(default_pkgs + generators);
+	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;
 
 	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
 	let default_generator = nix_go!(call_package(generator)(Obj {}));
@@ -394,7 +394,7 @@
 				default_generator,
 				expected_owners,
 				expected_generation_data,
-				batch,
+				// batch,
 			)
 			.await
 		}
@@ -416,7 +416,7 @@
 	secret: Value,
 	expected_owners: Vec<String>,
 	expected_generation_data: serde_json::Value,
-	batch: Option<NixBuildBatch>,
+	// batch: Option<NixBuildBatch>,
 ) -> Result<FleetSharedSecret> {
 	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
 	Ok(FleetSharedSecret {
@@ -426,7 +426,7 @@
 			secret,
 			&expected_owners,
 			expected_generation_data,
-			batch,
+			// batch,
 		)
 		.await?,
 		owners: expected_owners,
@@ -722,7 +722,8 @@
 				}
 
 				let config_field = &config.config_field;
-				let field = nix_go!(config_field.sharedSecrets[{ name }]);
+				let name_clone = name.clone();
+				let field = nix_go!(config_field.sharedSecrets[name_clone]);
 				let expected_generation_data = nix_go_json!(field.expectedGenerationData);
 
 				let updated = maybe_regenerate_shared_secret(
@@ -733,7 +734,7 @@
 					&target_machines,
 					expected_generation_data,
 					&prefer_identities,
-					None,
+					// None,
 				)
 				.await?;
 				config.replace_shared(name, updated);
@@ -746,7 +747,7 @@
 				let stored_shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();
 				{
 					// Generate missing shared
-					let shared_batch = None;
+					// let shared_batch = None;
 					let _span = info_span!("shared").entered();
 					let expected_shared_set = config
 						.list_configured_shared()
@@ -771,7 +772,7 @@
 							secret,
 							expected_owners,
 							expected_generation_data,
-							shared_batch.clone(),
+							// shared_batch.clone(),
 						)
 						.in_current_span()
 						.await?;
@@ -779,7 +780,7 @@
 					}
 				}
 				if !skip_hosts {
-					let hosts_batch = None;
+					// let hosts_batch = None;
 					for host in config.list_hosts().await? {
 						if opts.should_skip(&host).await? {
 							continue;
@@ -805,9 +806,9 @@
 								config,
 								missing,
 								secret,
-								&[host.name.clone()],
+								slice::from_ref(&host.name),
 								expected_generation_data,
-								hosts_batch.clone(),
+								// hosts_batch.clone(),
 							)
 							.in_current_span()
 							.await
@@ -831,9 +832,9 @@
 									config,
 									&name,
 									secret,
-									&[host.name.clone()],
+									slice::from_ref(&host.name),
 									expected_generation_data,
-									hosts_batch.clone(),
+									// hosts_batch.clone(),
 								)
 								.in_current_span()
 								.await
@@ -874,7 +875,7 @@
 							&expected_owners,
 							expected_generation_data,
 							&prefer_identities,
-							None,
+							// None,
 						)
 						.await?,
 					);
modifiedcmds/fleet/src/cmds/tf.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/tf.rs
+++ b/cmds/fleet/src/cmds/tf.rs
@@ -42,8 +42,7 @@
 			debug!("generating terraform configs");
 			let system = &config.local_system;
 			let config = &config.config_field;
-			let data: HashMap<String, PathBuf> = nix_go!(config.tf({ system })).build().await?;
-			let data = &data["out"];
+			let data: PathBuf = nix_go!(config.tf({ system })).build("out").await?;
 			let data = fs::read(&data).await?;
 
 			create_dir_all(&dir).await?;
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -23,6 +23,7 @@
 use human_repr::HumanCount;
 #[cfg(feature = "indicatif")]
 use indicatif::{ProgressState, ProgressStyle};
+use nix_eval::{gc_register_my_thread, gc_unregister_my_thread, init_libraries};
 use tracing::{Instrument, error, info, info_span};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::IndicatifLayer;
@@ -183,21 +184,31 @@
 	}
 
 	setup_logging();
-	async_main(opts)
-}
 
-#[tokio::main]
-async fn async_main(opts: RootOpts) -> ExitCode {
-	if let Err(e) = main_real(opts).await {
-		error!("{e:#}");
-		return ExitCode::FAILURE;
-	}
-	ExitCode::SUCCESS
+	init_libraries();
+
+	tokio::runtime::Builder::new_multi_thread()
+		.enable_all()
+		.on_thread_start(|| {
+			gc_register_my_thread();
+		})
+		.on_thread_stop(|| {
+			gc_unregister_my_thread();
+		})
+		.build()
+		.expect("failed to build runtime")
+		.block_on(async {
+			if let Err(e) = main_real(opts).await {
+				error!("{e:#}");
+				ExitCode::FAILURE
+			} else {
+				ExitCode::SUCCESS
+			}
+		})
+	// async_main(opts)
 }
 
 async fn main_real(opts: RootOpts) -> Result<()> {
-	nix_eval::init_tokio();
-
 	let nix_args = std::env::var_os("NIX_ARGS")
 		.map(|a| extra_args::parse_os(&a))
 		.transpose()?
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -12,7 +12,7 @@
 
 use anyhow::{Context, Result, anyhow, bail, ensure};
 use fleet_shared::SecretData;
-use nix_eval::{NixSession, Value, nix_go, nix_go_json, util::assert_warn};
+use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
 use openssh::SessionBuilder;
 use serde::de::DeserializeOwned;
 use tabled::Tabled;
@@ -41,8 +41,6 @@
 	pub default_pkgs: Value,
 	/// inputs.nixpkgs
 	pub nixpkgs: Value,
-
-	pub nix_session: NixSession,
 }
 
 // TODO: Make field not pub
@@ -232,14 +230,17 @@
 			}
 		};
 		if !is_fleet_managed {
-			bail!(indoc::indoc! {"
+			bail!(
+				"{}",
+				indoc::indoc! {"
 				host is not marked as managed by fleet
 				if you're not trying to lustrate/install system from scratch,
 				you should either
 					1. manually create /etc/FLEET_HOST file on the target host,
 					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet
 					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos
-			"});
+			"}
+			);
 		}
 		// TOCTOU is possible
 		let _ = self.deploy_kind.set(DeployKind::Fleet);
@@ -503,8 +504,8 @@
 		let nixos = self.nixos_unchecked_config().await?;
 		let secrets = nix_go!(nixos.secrets);
 		let mut out = Vec::new();
-		for name in secrets.list_fields().await? {
-			let secret = nix_go!(secrets[{ name }]);
+		for name in secrets.list_fields()? {
+			let secret = secrets.get_field(&name)?;
 			let is_shared: bool = nix_go_json!(secret.shared);
 			if is_shared {
 				continue;
@@ -592,7 +593,7 @@
 	}
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
 		let config = &self.config_field;
-		let names = nix_go!(config.hosts).list_fields().await?;
+		let names = nix_go!(config.hosts).list_fields()?;
 		let mut out = vec![];
 		for name in names {
 			out.push(self.host(&name).await?);
@@ -608,7 +609,7 @@
 	/// Shared secrets configured in fleet.nix or in flake
 	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
 		let config_field = &self.config_field;
-		Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)
+		nix_go!(config_field.sharedSecrets).list_fields()
 	}
 	/// Shared secrets configured in fleet.nix
 	pub fn list_shared(&self) -> Vec<String> {
@@ -680,10 +681,10 @@
 	// maybe it can be a .nix file for persistence, but accessible only
 	// thru some shared state controller? Might it be stored in terraform
 	// state provider?
-	pub fn data(&self) -> MutexGuard<FleetData> {
+	pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {
 		self.data.lock().unwrap()
 	}
-	pub fn data_mut(&self) -> MutexGuard<FleetData> {
+	pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {
 		self.data.lock().unwrap()
 	}
 	pub fn save(&self) -> Result<()> {
@@ -691,8 +692,7 @@
 		let data = nixlike::serialize(&self.data() as &FleetData)?;
 		tempfile.write_all(
 			format!(
-				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",
-				data
+				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"
 			)
 			.as_bytes(),
 		)?;
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -7,7 +7,7 @@
 };
 
 use anyhow::{Context, Result, bail};
-use nix_eval::{NixSessionPool, Value, nix_go, util::assert_warn};
+use nix_eval::{FetchSettings, FlakeReference, FlakeSettings, Value, nix_go, util::assert_warn};
 use nom::{
 	Parser,
 	bytes::complete::take_while1,
@@ -210,30 +210,39 @@
 			std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
 		let data: Mutex<FleetData> = nixlike::parse_str(&bytes)?;
 
-		let pool = NixSessionPool::new(
-			directory.as_os_str().to_owned(),
-			nix_args.clone(),
-			self.local_system.clone(),
-			self.fail_fast,
-		)
-		.await?;
-		let nix_session = pool.get().await?;
+		let mut fetch_settings = FetchSettings::new();
+		fetch_settings.set(c"warn-dirty", c"false");
+
+		// TODO: use correct directory, not cwd
+		let (mut flake, _) = FlakeReference::new(
+			directory
+				.to_str()
+				.ok_or_else(|| anyhow::anyhow!("fleet dir should have utf-8 path"))?,
+			&fetch_settings,
+		)?;
+		let flake = flake.lock(&fetch_settings)?;
+
+		let mut flake_settings = FlakeSettings::new()?;
+		let flake = flake.get_attrs(&mut flake_settings)?;
 
-		let builtins_field = Value::binding(nix_session.clone(), "builtins").await?;
+		let builtins_field = Value::eval("builtins")?;
 
-		let fleet_root = Value::binding(nix_session.clone(), "fleetConfigurations").await?;
-		let fleet_field = nix_go!(fleet_root.default({ data }));
+		let fleet_root = flake.get_field("fleetConfigurations")?;
+		let data_val = Value::serialized(&data)?;
+		let fleet_field = nix_go!(fleet_root.default(data_val));
 
 		let config_field = nix_go!(fleet_field.config);
 
 		if assert {
-			assert_warn("fleet config evaluation", &config_field).await?;
+			assert_warn("fleet config evaluation", &config_field)
+				.await
+				.context("failed to verify assertions")?;
 		}
 
 		let import = nix_go!(builtins_field.import);
 		let overlays = nix_go!(config_field.nixpkgs.overlays);
 		let nixpkgs = nix_go!(config_field.nixpkgs.buildUsing);
-		let nixpkgs_imported = nix_go!(nixpkgs | import);
+		let nixpkgs_imported = nix_go!(import(nixpkgs));
 
 		let default_pkgs = nix_go!(nixpkgs_imported(Obj {
 			overlays,
@@ -241,7 +250,6 @@
 		}));
 
 		Ok(Config(Arc::new(FleetConfigInternals {
-			nix_session,
 			directory,
 			data,
 			local_system: self.local_system.clone(),
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -16,12 +16,17 @@
 tokio-util.workspace = true
 tracing.workspace = true
 
+cxx = "1.0.168"
 futures = "0.3.31"
 itertools = "0.14.0"
 r2d2 = "0.8.10"
 regex = "1.11.1"
+test-log = { version = "0.2.18", features = ["trace"] }
 unindent = "0.2.4"
+tracing-indicatif = "0.3.13"
+ctor = "0.5.0"
 
-# [build-dependencies]
-# bindgen = "0.69.4"
-# pkg-config = "0.3.30"
+[build-dependencies]
+bindgen = "0.72.0"
+cxx-build = "1.0.168"
+pkg-config = "0.3.30"
modifiedcrates/nix-eval/build.rsdiffbeforeafterboth
--- a/crates/nix-eval/build.rs
+++ b/crates/nix-eval/build.rs
@@ -1,30 +1,101 @@
-// use bindgen::callbacks::ParseCallbacks;
-// use std::path::PathBuf;
-//
-// #[derive(Debug)]
-// struct StripPrefix;
-// impl ParseCallbacks for StripPrefix {
-//     fn item_name(&self, name: &str) -> Option<String> {
-//         name.strip_prefix("nix_").map(ToOwned::to_owned)
-//     }
-// }
+use bindgen::{
+	RustEdition,
+	callbacks::{ItemInfo, ParseCallbacks},
+};
+use std::path::PathBuf;
 
+#[derive(Debug)]
+struct StripPrefix;
+impl ParseCallbacks for StripPrefix {
+	fn item_name(&self, name: ItemInfo<'_>) -> Option<String> {
+		name.name.strip_prefix("nix_").map(ToOwned::to_owned)
+	}
+}
+
 fn main() {
-	//
-	// let mut libnix = bindgen::builder().header_contents("nix.h", "
-	// 	#define GC_THREADS
-	// 	#include <gc/gc.h>
-	// 	#include <nix_api_expr.h>
-	// 	#include <nix_api_store.h>
-	// 	#include <nix_api_util.h>
-	// 	#include <nix_api_value.h>
-	// ").parse_callbacks(Box::new(StripPrefix));
-	//
-	// for header in pkg_config::probe_library("nix-expr-c").expect("nix-expr-c").include_paths.into_iter().chain(pkg_config::probe_library("bdw-gc").expect("bdw-gc").include_paths.into_iter()) {
-	// 	libnix = libnix.clang_arg(format!("-I{}", header.to_str().expect("path is utf-8")));
-	// }
+	// Link nix C++ libraries for cxx
+	for lib in &[
+		"nix-util",
+		"nix-store",
+		"nix-expr",
+		"nix-flake",
+		"nix-fetchers",
+		"bdw-gc",
+	] {
+		if let Ok(library) = pkg_config::probe_library(lib) {
+			for lib_path in library.libs {
+				println!("cargo:rustc-link-lib={lib_path}");
+			}
+			for search_path in library.link_paths {
+				println!("cargo:rustc-link-search=native={}", search_path.display());
+			}
+		}
+	}
+
+	cxx_build::bridge("src/logging.rs")
+		.file("src/logging.cc")
+		.std("c++20")
+		.shared_flag(true)
+		.compile("nix-eval-logging");
+	cxx_build::bridge("src/lib.rs")
+		.file("src/lib.cc")
+		.std("c++20")
+		.shared_flag(true)
+		.compile("nix-eval");
+
+	println!("cargo:rerun-if-changed=src/lib.cc");
+	println!("cargo:rerun-if-changed=src/lib.hh");
+	println!("cargo:rerun-if-changed=src/logging.cc");
+	println!("cargo:rerun-if-changed=src/logging.hh");
+
 	//
-	// let mut out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo"));
-	// out.push("bindings.rs");
-	// libnix.generate().expect("generate bindings").write_to_file(out).expect("write bindings");
+	let mut libnix = bindgen::builder()
+		.rust_edition(RustEdition::Edition2024)
+		.header_contents(
+			"nix.h",
+			"
+				#define GC_THREADS
+				#include <gc/gc.h>
+				#include <nix_api_expr.h>
+				#include <nix_api_store.h>
+				#include <nix_api_flake.h>
+				#include <nix_api_fetchers.h>
+				#include <nix_api_util.h>
+				#include <nix_api_value.h>
+			",
+		)
+		.parse_callbacks(Box::new(StripPrefix));
+
+	for header in pkg_config::probe_library("nix-expr-c")
+		.expect("nix-expr-c")
+		.include_paths
+		.into_iter()
+		.chain(
+			pkg_config::probe_library("nix-flake-c")
+				.expect("nix-flake-c")
+				.include_paths
+				.into_iter(),
+		)
+		.chain(
+			pkg_config::probe_library("nix-fetchers-c")
+				.expect("nix-fetchers-c")
+				.include_paths
+				.into_iter(),
+		)
+		.chain(
+			pkg_config::probe_library("bdw-gc")
+				.expect("bdw-gc")
+				.include_paths
+				.into_iter(),
+		) {
+		libnix = libnix.clang_arg(format!("-I{}", header.to_str().expect("path is utf-8")));
+	}
+
+	let mut out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo"));
+	out.push("bindings.rs");
+	libnix
+		.generate()
+		.expect("generate bindings")
+		.write_to_file(out)
+		.expect("write bindings");
 }
addedcrates/nix-eval/src/lib.ccdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/lib.cc
@@ -0,0 +1,17 @@
+#include "nix-eval/src/lib.rs"
+#include "lib.hh"
+#include <nix/fetchers/fetch-settings.hh>
+#include <nix/util/ref.hh>
+#include <nix_api_fetchers.h>
+
+struct nix_fetchers_settings {
+  nix::ref<nix::fetchers::Settings> settings;
+};
+
+extern "C" {
+void set_fetcher_setting(nix_fetchers_settings *settings_struct,
+                         const char *setting, const char *value) {
+  auto &settings_ref = settings_struct->settings;
+  bool result = settings_ref->set(setting, value);
+}
+}
addedcrates/nix-eval/src/lib.hhdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/lib.hh
@@ -0,0 +1,7 @@
+#pragma once
+#include <nix_api_fetchers.h>
+
+extern "C" {
+void set_fetcher_setting(nix_fetchers_settings *settings, const char *setting,
+                         const char *value);
+}
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -3,120 +3,892 @@
 //!
 //! Current api is awful, little effort was put into this implementation.
 
-use std::{collections::HashMap, path::PathBuf, sync::Arc};
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};
+use std::fmt;
+use std::ptr::null_mut;
+use std::sync::LazyLock;
+use std::{collections::HashMap, path::PathBuf};
 
-pub use pool::NixSessionPool;
-use pool::NixSessionPoolInner;
-use r2d2::PooledConnection;
-pub use session::{Error, Result};
-use tokio::sync::{mpsc, oneshot};
-use tracing::instrument;
-pub use value::{Index, Value};
+use anyhow::{Context, bail};
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+
+pub use anyhow::Result;
+
+use self::logging::nix_logging_cxx;
+use self::nix_cxx::set_fetcher_setting;
+use self::nix_raw::{
+	alloc_value, c_context, c_context_create, err_code, err_info_msg, eval_state_build,
+	eval_state_builder_new, 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_parse_flags, flake_reference_parse_flags_free,
+	flake_reference_parse_flags_new, flake_reference_parse_flags_set_base_directory,
+	flake_settings, flake_settings_free, flake_settings_new, init_bool, init_int, init_string,
+	locked_flake_free, locked_flake_get_output_attrs, set_err_msg, setting_set, state_free,
+	value_decref, value_force, value_incref,
+};
 
-mod pool;
-mod session;
 mod value;
 // Contains macros helpers
+pub mod logging;
 #[doc(hidden)]
 pub mod macros;
 pub mod util;
-// #[allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
-// mod nix_raw {
-// 	include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
-// }
 
-// fn init() {
-// 	nix_raw::libutil_init();
-// }
+#[allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
+mod nix_raw {
+	include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+#[cxx::bridge]
+pub mod nix_cxx {
+	unsafe extern "C++" {
+		type nix_fetchers_settings;
+		include!("nix-eval/src/lib.hh");
+
+		unsafe fn set_fetcher_setting(
+			settings: *mut nix_fetchers_settings,
+			setting: *const c_char,
+			value: *const c_char,
+		);
+	}
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum NixType {
+	Thunk,
+	Int,
+	Float,
+	Bool,
+	String,
+	Path,
+	Null,
+	Attrs,
+	List,
+	Function,
+	External,
+}
+impl NixType {
+	fn from_int(c: c_uint) -> Self {
+		match c {
+			0 => Self::Thunk,
+			1 => Self::Int,
+			2 => Self::Float,
+			3 => Self::Bool,
+			4 => Self::String,
+			5 => Self::Path,
+			6 => Self::Null,
+			7 => Self::Attrs,
+			8 => Self::List,
+			9 => Self::Function,
+			10 => Self::External,
+			_ => unreachable!("unknown nix type: {c}"),
+		}
+	}
+}
+
+#[derive(Debug)]
+#[repr(i32)]
+enum NixErrorKind {
+	Unknown = 1,
+	Overflow = 2,
+	Key = 3,
+	Generic = 4,
+}
+impl NixErrorKind {
+	fn from_int(v: c_int) -> Option<Self> {
+		Some(match v {
+			0 => return None,
+			-1 => Self::Unknown,
+			-2 => Self::Overflow,
+			-3 => Self::Key,
+			-4 => Self::Generic,
+			_ => {
+				debug_assert!(false, "unexpected nix error kind: {v}");
+				Self::Unknown
+			}
+		})
+	}
+}
+
+pub fn gc_register_my_thread() {
+	assert_eq!(unsafe { nix_raw::GC_thread_is_registered() }, 0);
+
+	let mut sb = nix_raw::GC_stack_base {
+		mem_base: null_mut(),
+	};
+	let r = unsafe { nix_raw::GC_get_stack_base(&mut sb) };
+	if r as u32 != nix_raw::GC_SUCCESS {
+		panic!("failed to get thread stack base");
+	}
+	unsafe { nix_raw::GC_register_my_thread(&sb) };
+}
+pub fn gc_unregister_my_thread() {
+	assert_eq!(unsafe { nix_raw::GC_thread_is_registered() }, 1);
+
+	unsafe { nix_raw::GC_unregister_my_thread() };
+}
+
+struct ThreadRegisterGuard {}
+impl ThreadRegisterGuard {
+	fn new() -> Self {
+		gc_register_my_thread();
+		Self {}
+	}
+}
+impl Drop for ThreadRegisterGuard {
+	fn drop(&mut self) {
+		gc_unregister_my_thread();
+	}
+}
+
+struct NixContext(*mut c_context);
+impl NixContext {
+	fn set_err(&mut self, err: NixErrorKind, msg: &CStr) {
+		unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };
+	}
+	fn new() -> Self {
+		let ctx = unsafe { c_context_create() };
+		Self(ctx)
+	}
+	fn error_kind(&self) -> Option<NixErrorKind> {
+		let code = unsafe { err_code(self.0) };
+		NixErrorKind::from_int(code)
+	}
+	fn error<'t>(&self) -> Option<Cow<'t, str>> {
+		if let NixErrorKind::Generic = self.error_kind()? {
+			let mut err_out = String::new();
+			unsafe {
+				err_info_msg(
+					null_mut(),
+					self.0,
+					Some(copy_nix_str),
+					(&raw mut err_out).cast(),
+				)
+			};
+			return Some(Cow::Owned(err_out));
+		};
+
+		// TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,
+		// but it looks ugly
+		let str = unsafe { nix_raw::err_msg(null_mut(), self.0, null_mut()) };
+		Some(unsafe { CStr::from_ptr(str) }.to_string_lossy())
+
+		// TODO: There is also nix_err_info_msg, but I don't understand when it should be used
+		// Some(match self.error_kind()? {
+		// 	NixErrorKind::Generic => {
+		// 	}
+		// })
+	}
+	fn clean_err(&mut self) {
+		unsafe {
+			nix_raw::clear_err(self.0);
+		}
+	}
+
+	fn bail_if_error(&self) -> Result<()> {
+		if let Some(err) = self.error() {
+			bail!("{err}");
+		};
+		Ok(())
+	}
+
+	fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {
+		self.clean_err();
+		let o = f(self.0);
+		self.bail_if_error()?;
+		self.clean_err();
+		Ok(o)
+	}
+}
+impl Drop for NixContext {
+	fn drop(&mut self) {
+		unsafe {
+			nix_raw::c_context_free(self.0);
+		}
+	}
+}
+struct GlobalState {
+	store: Store,
+	state: EvalState,
+}
+impl GlobalState {
+	fn new() -> Result<Self> {
+		let mut ctx = NixContext::new();
+		let store = ctx
+			.run_in_context(|c| unsafe { nix_raw::store_open(c, c"daemon".as_ptr(), null_mut()) })
+			.map(Store)?;
+
+		let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;
+		ctx.run_in_context(|c| {
+			unsafe {
+				nix_raw::eval_state_builder_set_eval_setting(
+					c,
+					builder,
+					c"lazy-trees".as_ptr(),
+					c"true".as_ptr(),
+				)
+			}
+			// eval_s
+		})?;
+		let state = ctx
+			.run_in_context(|c| unsafe { eval_state_build(c, builder) })
+			.map(EvalState)?;
+
+		Ok(Self { store, state })
+	}
+}
+
+struct ThreadState {
+	ctx: NixContext,
+}
+impl ThreadState {
+	fn new() -> Result<Self> {
+		let ctx = NixContext::new();
+
+		Ok(Self { ctx })
+	}
+}
+
+static GLOBAL_STATE: LazyLock<GlobalState> =
+	LazyLock::new(|| GlobalState::new().expect("global state init shouldn't fail"));
+
+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 nix_raw::EvalState) -> 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);
+	let v = ctx.run_in_context(|c| f(c, state));
+	// It is reused for thread
+	std::mem::forget(ctx);
+	v
+}
+
+fn set_setting(s: &CStr, v: &CStr) -> Result<()> {
+	with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())
+}
+
+pub struct FetchSettings(*mut fetchers_settings);
+impl FetchSettings {
+	pub fn new() -> Self {
+		Self::try_new().expect("allocation should not fail")
+	}
+	fn try_new() -> Result<Self> {
+		with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)
+	}
+	pub fn set(&mut self, setting: &CStr, value: &CStr) {
+		unsafe {
+			set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());
+		};
+	}
+}
+unsafe impl Send for FetchSettings {}
+unsafe impl Sync for FetchSettings {}
+impl Drop for FetchSettings {
+	fn drop(&mut self) {
+		unsafe { fetchers_settings_free(self.0) };
+	}
+}
+pub struct FlakeSettings(*mut flake_settings);
+impl FlakeSettings {
+	pub fn new() -> Result<Self> {
+		with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)
+	}
+}
+unsafe impl Send for FlakeSettings {}
+unsafe impl Sync for FlakeSettings {}
+impl Drop for FlakeSettings {
+	fn drop(&mut self) {
+		unsafe {
+			flake_settings_free(self.0);
+		}
+	}
+}
+
+struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);
+impl FlakeReferenceParseFlags {
+	fn new(settings: &mut FlakeSettings) -> Result<Self> {
+		with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })
+			.map(Self)
+	}
+	fn set_base_dir(&mut self, dir: &str) -> Result<()> {
+		with_default_context(|c, _| {
+			unsafe {
+				flake_reference_parse_flags_set_base_directory(
+					c,
+					self.0,
+					dir.as_ptr().cast(),
+					dir.len(),
+				)
+			};
+		})
+	}
+}
+impl Drop for FlakeReferenceParseFlags {
+	fn drop(&mut self) {
+		unsafe {
+			flake_reference_parse_flags_free(self.0);
+		}
+	}
+}
+struct FlakeLockFlags(*mut flake_lock_flags);
+impl FlakeLockFlags {
+	fn new(settings: &mut FlakeSettings) -> Result<Self> {
+		with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) }).map(Self)
+	}
+}
+impl Drop for FlakeLockFlags {
+	fn drop(&mut self) {
+		unsafe {
+			flake_lock_flags_free(self.0);
+		}
+	}
+}
+
+unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {
+	let s = unsafe { std::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() };
+}
+
+struct Store(*mut nix_raw::Store);
+unsafe impl Send for Store {}
+unsafe impl Sync for Store {}
+
+struct EvalState(*mut nix_raw::EvalState);
+impl EvalState {
+	// TODO: store ownership
+	fn new_raw(store: *mut nix_raw::Store) -> Result<Self> {
+		let builder =
+			with_default_context(|c, _| unsafe { nix_raw::eval_state_builder_new(c, store) })?;
+
+		with_default_context(|c, _| unsafe { eval_state_build(c, builder) }).map(Self)
+
+		// with_default_context(|c| state_create(c))
+	}
+}
+unsafe impl Send for EvalState {}
+unsafe impl Sync for EvalState {}
+impl Drop for EvalState {
+	fn drop(&mut self) {
+		unsafe {
+			state_free(self.0);
+		}
+	}
+}
+
+pub struct FlakeReference(*mut nix_raw::flake_reference);
+impl FlakeReference {
+	pub fn new(s: &str, fetch: &FetchSettings) -> Result<(Self, String)> {
+		let mut flake_settings = FlakeSettings::new()?;
+		let mut parse_flags = FlakeReferenceParseFlags::new(&mut flake_settings)?;
+
+		// parse_flags.set_base_dir("/home/lach/build/fleet")?;
+
+		let mut out = null_mut();
+		let mut fragment = String::new();
+		// let fetch_settings = fetcher_settings;
+		with_default_context(|c, _| unsafe {
+			nix_raw::flake_reference_and_fragment_from_string(
+				c,
+				fetch.0,
+				flake_settings.0,
+				parse_flags.0,
+				s.as_ptr().cast(),
+				s.len(),
+				&mut out,
+				Some(copy_nix_str),
+				(&raw mut fragment).cast(),
+			)
+		})?;
+		assert!(!out.is_null());
+
+		Ok((Self(out), fragment))
+	}
+	pub fn lock(&mut self, fetch: &FetchSettings) -> Result<LockedFlake> {
+		let mut settings = FlakeSettings::new()?;
+		let lock_flags = FlakeLockFlags::new(&mut settings)?;
+		with_default_context(|c, es| unsafe {
+			flake_lock(c, fetch.0, settings.0, es, lock_flags.0, self.0)
+		})
+		.map(LockedFlake)
+	}
+}
+unsafe impl Send for FlakeReference {}
+unsafe impl Sync for FlakeReference {}
+
+pub struct LockedFlake(*mut nix_raw::locked_flake);
+impl LockedFlake {
+	pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {
+		with_default_context(|c, es| unsafe {
+			locked_flake_get_output_attrs(c, settings.0, es, self.0)
+		})
+		.map(Value)
+	}
+}
+unsafe impl Send for LockedFlake {}
+unsafe impl Sync for LockedFlake {}
+impl Drop for LockedFlake {
+	fn drop(&mut self) {
+		unsafe {
+			locked_flake_free(self.0);
+		};
+	}
+}
+
+type FieldName = [u8; 32];
+fn init_field_name(v: &str) -> FieldName {
+	let mut f = [0; 32];
+	assert!(v.len() < 32, "max field name is 31 char");
+	assert!(
+		v.bytes().all(|v| v != 0),
+		"nul bytes are unsupported in field name"
+	);
+	f[0..v.len()].copy_from_slice(v.as_bytes());
+	f
+}
+
+pub struct RealisedString(*mut nix_raw::realised_string);
+impl fmt::Debug for RealisedString {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		self.as_str().fmt(f)
+	}
+}
+
+impl RealisedString {
+	fn as_str(&self) -> &str {
+		let len = unsafe { nix_raw::realised_string_get_buffer_size(self.0) };
+		let data: *const u8 = unsafe { nix_raw::realised_string_get_buffer_start(self.0) }.cast();
+		let data = unsafe { std::slice::from_raw_parts(data, len) };
+		std::str::from_utf8(data).expect("non-utf8 strings not supported")
+	}
+	fn path_count(&self) -> usize {
+		unsafe { nix_raw::realised_string_get_store_path_count(self.0) }
+	}
+	fn path(&self, i: usize) -> String {
+		assert!(i < self.path_count());
+		let path = unsafe { nix_raw::realised_string_get_store_path(self.0, i) };
+		let mut err_out = String::new();
+		unsafe { nix_raw::store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };
+		err_out
+	}
+}
+
+unsafe impl Send for RealisedString {}
+impl Drop for RealisedString {
+	fn drop(&mut self) {
+		with_default_context(|c, _| unsafe { nix_raw::realised_string_free(self.0) })
+			.expect("string free should not fail")
+	}
+}
 
-#[derive(Clone)]
-pub struct NixSession(pub(crate) Arc<tokio::sync::Mutex<PooledConnection<NixSessionPoolInner>>>);
+pub struct Value(*mut nix_raw::value);
 
-struct NixBuildTask(Value, oneshot::Sender<Result<HashMap<String, PathBuf>>>);
+unsafe impl Send for Value {}
+unsafe impl Sync for Value {}
 
-#[derive(Clone)]
-pub struct NixBuildBatch {
-	tx: mpsc::UnboundedSender<NixBuildTask>,
+pub trait AsFieldName {
+	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;
+	fn to_field_name(&self) -> Result<String>;
+}
+impl AsFieldName for Value {
+	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {
+		let f = self.to_string()?;
+		v(init_field_name(&f))
+	}
+	fn to_field_name(&self) -> Result<String> {
+		self.to_string()
+	}
+}
+impl<E> AsFieldName for E
+where
+	E: AsRef<str>,
+{
+	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {
+		let f = self.as_ref();
+		v(init_field_name(f))
+	}
+	fn to_field_name(&self) -> Result<String> {
+		Ok(self.as_ref().to_owned())
+	}
 }
 
-#[instrument(skip(session, values))]
-async fn build_multiple(name: String, session: NixSession, values: Vec<Value>) -> Result<()> {
-	let system = session.0.lock().await.nix_system.clone();
-	let builtins = Value::binding(session, "builtins").await?;
-	let drv = nix_go!(builtins.derivation(Obj {
-		system,
-		name,
-		builder: "/bin/sh",
-		// we want nothing from this derivation, it is only used to perform multiple builds at once.
-		args: vec!["-c", "echo > $out"],
-		preferLocalBuild: true,
-		allowSubstitutes: false,
-		buildInputs: values,
-	}));
-	drv.build().await?;
-	Ok(())
+struct AttrsBuilder(*mut nix_raw::BindingsBuilder);
+impl AttrsBuilder {
+	fn new(capacity: usize) -> Self {
+		with_default_context(|c, es| unsafe { nix_raw::make_bindings_builder(c, es, capacity) })
+			.map(Self)
+			.expect("alloc should not fail")
+	}
+	fn insert(&mut self, k: &impl AsFieldName, v: Value) {
+		k.as_field_name(|name| {
+			with_default_context(|c, _| unsafe {
+				nix_raw::bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0)
+			})
+		})
+		.expect("builder insert shouldn't fail");
+	}
 }
+impl Drop for AttrsBuilder {
+	fn drop(&mut self) {
+		unsafe { nix_raw::bindings_builder_free(self.0) };
+	}
+}
+
+impl Value {
+	pub fn new_attrs(v: HashMap<&str, Value>) -> Result<Self> {
+		let out = Self::new_uninit()?;
+		let mut b = AttrsBuilder::new(v.len());
+		for (k, v) in v {
+			b.insert(&k, v);
+		}
+		with_default_context(|c, _| unsafe { nix_raw::make_attrs(c, out.0, b.0) })?;
+		Ok(out)
+	}
+	fn new_list<T: Into<Self>>(v: Vec<T>) -> Result<Self> {
+		todo!()
+	}
+	fn new_uninit() -> Result<Self> {
+		let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })?;
+		Ok(Self(out))
+	}
+	fn new_str(v: &str) -> Result<Self> {
+		let s = CString::new(v).expect("string should not contain NULs");
+		let uninit = Self::new_uninit()?;
+		// String is copied, `s` is free to be dropped
+		with_default_context(|c, _| unsafe { init_string(c, uninit.0, s.as_ptr()) })?;
+		Ok(uninit)
+	}
+	fn new_int(i: i64) -> Result<Self> {
+		let uninit = Self::new_uninit()?;
+		with_default_context(|c, _| unsafe { init_int(c, uninit.0, i) })?;
+		Ok(uninit)
+	}
+	fn new_bool(v: bool) -> Result<Self> {
+		let uninit = Self::new_uninit()?;
+		with_default_context(|c, _| unsafe { init_bool(c, uninit.0, v) })?;
+		Ok(uninit)
+	}
+	fn force(&mut self, st: &mut EvalState) -> Result<()> {
+		with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;
+		Ok(())
+	}
+	pub fn type_of(&self) -> Result<NixType> {
+		let ty = with_default_context(|c, _| unsafe { nix_raw::get_type(c, self.0) })?;
+		Ok(NixType::from_int(ty))
+	}
+	pub fn to_string(&self) -> Result<String> {
+		Ok(self.to_realised_string()?.as_str().to_owned())
+	}
+	pub fn to_realised_string(&self) -> Result<RealisedString> {
+		with_default_context(|c, es| unsafe { nix_raw::string_realise(c, es, self.0, false) })
+			.map(RealisedString)
+
+		// let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };
+		// for i in 0..store_paths {
+		// 	let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };
+		// 	nix_raw::store_path_name(store_path, callback, user_data);
+		// }
+		// dbg!(store_paths);
+		// todo!();
+	}
+
+	pub fn has_field(&self, field: &str) -> Result<bool> {
+		let f = init_field_name(field);
+		with_default_context(|c, es| unsafe {
+			nix_raw::has_attr_byname(c, self.0, es, f.as_ptr().cast())
+		})
+	}
+	// pub fn derivation_path(&self) {
+	// 	nix_raw::real
+	// }
+	pub fn list_fields(&self) -> Result<Vec<String>> {
+		if !matches!(self.type_of()?, NixType::Attrs) {
+			bail!("invalid type: expected attrs");
+		}
 
-impl NixBuildBatch {
-	fn new(name: String, session: NixSession) -> Self {
-		let (tx, mut rx) = mpsc::unbounded_channel::<NixBuildTask>();
+		let len = with_default_context(|c, _| unsafe { nix_raw::get_attrs_size(c, self.0) })?;
+		let mut out = Vec::with_capacity(len as usize);
 
-		tokio::task::spawn(async move {
-			let mut deps = vec![];
-			let mut build_data = vec![];
-			while let Some(task) = rx.recv().await {
-				build_data.push(task.0.clone());
-				deps.push(task);
+		for i in 0..len {
+			let name = with_default_context(|c, es| unsafe {
+				nix_raw::get_attr_name_byidx(c, self.0, es, i)
+			})?;
+			let c = unsafe { CStr::from_ptr(name) };
+			out.push(c.to_str().expect("nix field names are utf-8").to_owned());
+		}
+		Ok(out)
+	}
+	pub fn get_elem(&self, v: usize) -> Result<Self> {
+		if !matches!(self.type_of()?, NixType::List) {
+			bail!("invalid type: expected list");
+		}
+		let len =
+			with_default_context(|c, _| unsafe { nix_raw::get_list_size(c, self.0) })? as usize;
+		if v >= len {
+			bail!("oob list get: {v} >= {len}");
+		}
+
+		with_default_context(|c, es| unsafe { nix_raw::get_list_byidx(c, self.0, es, v as u32) })
+			.map(Self)
+	}
+	pub fn attrs_update(self, other: Value) -> Result<Self> {
+		let a_fields = self.list_fields()?;
+		let b_fields = other.list_fields()?;
+		match (a_fields.len(), b_fields.len()) {
+			(_, 0) => return Ok(self),
+			(0, _) => return Ok(other),
+			_ => {}
+		}
+		let mut out = HashMap::new();
+		for f in a_fields.iter() {
+			if b_fields.contains(f) {
+				break;
 			}
-			if deps.is_empty() {
-				return;
+			out.insert(f.as_str(), self.get_field(f)?);
+		}
+		if out.is_empty() {
+			// All fields from lhs are overriden by rhs
+			return Ok(other);
+		}
+		for f in b_fields.iter() {
+			out.insert(f.as_str(), other.get_field(f)?);
+		}
+		Self::new_attrs(out)
+	}
+	pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {
+		if !matches!(self.type_of()?, NixType::Attrs) {
+			bail!("invalid type: expected attrs");
+		}
+
+		name.as_field_name(|name| {
+			with_default_context(|c, es| unsafe {
+				nix_raw::get_attr_byname(c, self.0, es, name.as_ptr().cast())
+			})
+			.map(Self)
+		})
+		.with_context(|| format!("getting field {:?}", name.to_field_name()))
+	}
+	pub fn call(&self, v: Value) -> Result<Self> {
+		if !matches!(self.type_of()?, NixType::Function) {
+			// TODO: Functors
+			bail!("invalid type: expected function");
+		}
+
+		let out = Value::new_uninit()?;
+		with_default_context(|c, es| unsafe { nix_raw::value_call(c, es, self.0, v.0, out.0) })?;
+
+		Ok(out)
+	}
+	pub fn eval(v: &str) -> Result<Self> {
+		let s = CString::new(v).expect("expression shouldn't have internal NULs");
+		let out = Self::new_uninit()?;
+		with_default_context(|c, es| unsafe {
+			expr_eval_from_string(c, es, s.as_ptr(), c"/homeless-shelter".as_ptr(), out.0)
+		})?;
+		Ok(out)
+	}
+	pub async fn build(&self, output: &str) -> Result<PathBuf> {
+		if !self.is_derivation() {
+			bail!("expected derivation to build")
+		}
+		let output_name = self.get_field("outputName")?.to_string()?;
+		let v = if output_name != output {
+			let out = self.get_field(output)?;
+			if !out.is_derivation() {
+				bail!("unknown output: {output}");
 			}
-			match build_multiple(name, session, build_data).await {
-				Ok(_) => {
-					for NixBuildTask(v, o) in deps {
-						let _ = o.send(v.build().await);
-					}
-				}
-				Err(e) => {
-					for NixBuildTask(v, o) in deps {
-						let s = v.to_string_weak().await;
-						let s = match s {
-							Ok(s) => s,
-							Err(e) => {
-								let _ = o.send(Err(e));
-								continue;
-							}
-						};
-						if PathBuf::from(s).exists() {
-							let _ = o.send(v.build().await);
-						} else {
-							let _ = o.send(Err(e.clone()));
-						}
-					}
-				}
-			};
-		});
-		Self { tx }
+			out
+		} else {
+			self.clone()
+		};
+		// to_string here blocks until the path is built
+		let drv_path = tokio::task::spawn_blocking(move || v.get_field("outPath")?.to_string())
+			.await
+			.expect("should not fail")?;
+		Ok(PathBuf::from(drv_path))
 	}
-	pub async fn submit(self, task: Value) -> Result<HashMap<String, PathBuf>> {
-		let Self { tx: task_tx } = self;
-		let (tx, rx) = oneshot::channel();
-		let _ = task_tx.send(NixBuildTask(task, tx));
-		drop(task_tx);
-		rx.await.expect("shoudn't be cancelled here")
+	pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {
+		let to_json = Self::eval("builtins.toJSON")?;
+		let s = to_json.call(self.clone())?.to_string()?;
+		Ok(serde_json::from_str(&s)?)
+	}
+	pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {
+		Self::eval(&nixlike::serialize(v)?)
+	}
+
+	// Convert to string/evaluate derivations/etc
+	fn to_string_weak(&self) -> Result<String> {
+		// TODO
+		self.to_string()
+	}
+
+	fn is_derivation(&self) -> bool {
+		if !matches!(self.type_of(), Ok(NixType::Attrs)) {
+			return false;
+		}
+		let Some(ty) = self.get_field("type").ok() else {
+			return false;
+		};
+		matches!(ty.to_string().as_deref(), Ok("derivation"))
 	}
 }
 
-impl NixSession {
-	fn ptr_eq(a: &Self, b: &Self) -> bool {
-		Arc::ptr_eq(&a.0, &b.0)
+impl From<String> for Value {
+	fn from(value: String) -> Self {
+		Value::new_str(&value).expect("todo: TryFrom")
 	}
+}
+impl From<bool> for Value {
+	fn from(value: bool) -> Self {
+		Value::new_bool(value).expect("todo: TryFrom")
+	}
+}
+impl From<&str> for Value {
+	fn from(value: &str) -> Self {
+		Value::new_str(&value).expect("todo: TryFrom")
+	}
+}
+impl<T> From<Vec<T>> for Value
+where
+	T: Into<Value>,
+{
+	fn from(value: Vec<T>) -> Self {
+		Value::new_list(value).expect("todo: TryFrom")
+	}
+}
 
-	pub fn new_build_batch(&self, name: String) -> NixBuildBatch {
-		NixBuildBatch::new(name, self.clone())
+impl Clone for Value {
+	fn clone(&self) -> Self {
+		with_default_context(|c, _| unsafe { value_incref(c, self.0) })
+			.expect("value incref should not fail");
+		Self(self.0)
 	}
 }
+impl Drop for Value {
+	fn drop(&mut self) {
+		with_default_context(|c, _| unsafe { value_decref(c, self.0) })
+			.expect("value drop should not fail");
+	}
+}
+
+pub fn init_libraries() {
+	unsafe { nix_raw::GC_allow_register_threads() };
 
-pub fn init_tokio() {
-	let _ = pool::TOKIO_RUNTIME.set(tokio::runtime::Handle::current());
+	let mut ctx = NixContext::new();
+	ctx.run_in_context(|c| unsafe { nix_raw::libutil_init(c) })
+		.expect("util init should not fail");
+	ctx.run_in_context(|c| unsafe { nix_raw::libstore_init(c) })
+		.expect("store init should not fail");
+	ctx.run_in_context(|c| unsafe { nix_raw::libexpr_init(c) })
+		.expect("expr init should not fail");
+
+	nix_logging_cxx::apply_tracing_logger();
 }
+
+#[test_log::test]
+fn test_native() -> Result<()> {
+	let mut fetch_settings = FetchSettings::new();
+	fetch_settings.set(c"warn-dirty", c"false");
+	//
+
+	let (mut r, _) = FlakeReference::new("/home/lach/build/fleet", &fetch_settings)?;
+	let locked = r.lock(&fetch_settings)?;
+	let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;
+
+	let builtins = Value::eval("builtins")?;
+	dbg!(builtins.type_of()?);
+
+	dbg!(attrs.type_of()?);
+	dbg!(attrs.list_fields()?);
+	dbg!(
+		attrs
+			.get_field("packages")?
+			.get_field("x86_64-linux")?
+			.get_field("fleet")?
+			.get_field("outPath")?
+			.to_string()
+	);
+
+	Ok(())
+}
+
+// struct NixBuildTask(Value, oneshot::Sender<Result<HashMap<String, PathBuf>>>);
+//
+// #[derive(Clone)]
+// pub struct NixBuildBatch {
+// 	tx: mpsc::UnboundedSender<NixBuildTask>,
+// }
+//
+// #[instrument(skip(values))]
+// async fn build_multiple(name: String, values: Vec<Value>) -> Result<()> {
+// 	let builtins = Value::eval("builtins")?;
+// 	let drv = nix_go!(builtins.derivation(Obj {
+// 		// FIXME: pass system from localSystem or fleet args
+// 		// system,
+// 		name,
+// 		builder: "/bin/sh",
+// 		// we want nothing from this derivation, it is only used to perform multiple builds at once.
+// 		args: vec!["-c", "echo > $out"],
+// 		preferLocalBuild: true,
+// 		allowSubstitutes: false,
+// 		buildInputs: values,
+// 	}));
+// 	drv.build()?;
+// 	Ok(())
+// }
+//
+// impl NixBuildBatch {
+// 	fn new(name: String) -> Self {
+// 		let (tx, mut rx) = mpsc::unbounded_channel::<NixBuildTask>();
+//
+// 		tokio::task::spawn(async move {
+// 			let mut deps = vec![];
+// 			let mut build_data = vec![];
+// 			while let Some(task) = rx.recv().await {
+// 				build_data.push(task.0.clone());
+// 				deps.push(task);
+// 			}
+// 			if deps.is_empty() {
+// 				return;
+// 			}
+// 			match build_multiple(name, build_data).await {
+// 				Ok(_) => {
+// 					for NixBuildTask(v, o) in deps {
+// 						let _ = o.send(v.build());
+// 					}
+// 				}
+// 				Err(e) => {
+// 					for NixBuildTask(v, o) in deps {
+// 						let s = v.to_string_weak();
+// 						let s = match s {
+// 							Ok(s) => s,
+// 							Err(e) => {
+// 								let _ = o.send(Err(e));
+// 								continue;
+// 							}
+// 						};
+// 						if PathBuf::from(s).exists() {
+// 							let _ = o.send(v.build());
+// 						} else {
+// 							let _ = o.send(Err(e.clone()));
+// 						}
+// 					}
+// 				}
+// 			};
+// 		});
+// 		Self { tx }
+// 	}
+// 	pub async fn submit(self, task: Value) -> Result<HashMap<String, PathBuf>> {
+// 		let Self { tx: task_tx } = self;
+// 		let (tx, rx) = oneshot::channel();
+// 		let _ = task_tx.send(NixBuildTask(task, tx));
+// 		drop(task_tx);
+// 		rx.await.expect("shoudn't be cancelled here")
+// 	}
+// }
addedcrates/nix-eval/src/logging.ccdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/logging.cc
@@ -0,0 +1,75 @@
+#include "nix-eval/src/logging.rs"
+#include "logging.hh"
+#include <nix/util/logging.hh>
+
+using namespace nix;
+
+struct TracingLogger : Logger {
+  TracingLogger() {}
+
+  bool isVerbose() override { return true; }
+  // void addFields(nlohmann::json & json, const Fields & fields)
+  //    {
+  //        if (fields.empty())
+  //            return;
+  //        auto & arr = json["fields"] = nlohmann::json::array();
+  //        for (auto & f : fields)
+  //            if (f.type == Logger::Field::tInt)
+  //                arr.push_back(f.i);
+  //            else if (f.type == Logger::Field::tString)
+  //                arr.push_back(f.s);
+  //            else
+  //                unreachable();
+  //    }
+  void log(Verbosity lvl, std::string_view s) override {
+    rust::Str str(s.data(), s.size());
+    emit_log(lvl, str);
+  }
+  void logEI(const ErrorInfo &ei) override { emit_log(ei.level, ei.msg.str()); }
+
+  void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
+                     const std::string &s, const Fields &fields,
+                     ActivityId parent) override {
+    auto b = new_start_activity(act, lvl, type);
+    for (auto &f : fields) {
+      if (f.type == Logger::Field::tInt) {
+        b->add_int_field(f.i);
+      } else if (f.type == Logger::Field::tString) {
+        b->add_string_field(f.s);
+      } else {
+        unreachable();
+      }
+    }
+    b->emit(parent, s);
+  };
+
+  void stopActivity(ActivityId act) override { emit_stop(act); };
+
+  void result(ActivityId act, ResultType type, const Fields &fields) override {
+    auto b = new_start_activity(act, 0, type);
+    for (auto &f : fields) {
+      if (f.type == Logger::Field::tInt) {
+        b->add_int_field(f.i);
+      } else if (f.type == Logger::Field::tString) {
+        b->add_string_field(f.s);
+      } else {
+        unreachable();
+      }
+    }
+    b->emit_result(type);
+  };
+
+  void writeToStdout(std::string_view s) override {
+    printf("writeToStdout() called\n");
+  }
+  void warn(const std::string &msg) override { emit_warn(msg); }
+
+  virtual std::optional<char> ask(std::string_view s) {
+    printf("ask() called\n");
+    return {};
+  }
+};
+
+extern "C" {
+void apply_tracing_logger() { logger = std::make_unique<TracingLogger>(); }
+}
addedcrates/nix-eval/src/logging.hhdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/logging.hh
@@ -0,0 +1,5 @@
+#pragma once
+
+extern "C" {
+void apply_tracing_logger();
+}
addedcrates/nix-eval/src/logging.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/logging.rs
@@ -0,0 +1,602 @@
+use std::collections::HashMap;
+use std::fmt::Arguments;
+use std::sync::{LazyLock, Mutex};
+
+use tracing::{
+	Level, Metadata, Span, debug, debug_span, error, error_span, event, info, info_span, trace,
+	trace_span, warn, warn_span,
+};
+use tracing_indicatif::span_ext::IndicatifSpanExt as _;
+
+#[derive(Debug)]
+enum ActivityType {
+	Unknown = 0,
+	CopyPath = 100,
+	FileTransfer = 101,
+	Realise = 102,
+	CopyPaths = 103,
+	Builds = 104,
+	Build = 105,
+	OptimiseStore = 106,
+	VerifyPaths = 107,
+	Substitute = 108,
+	QueryPathInfo = 109,
+	PostBuildHook = 110,
+	BuildWaiting = 111,
+	FetchTree = 112,
+}
+
+fn strip_prefix_suffix<'s, 'p>(a: &'s str, pref: &'p str, suff: &'p str) -> Option<&'s str> {
+	a.strip_prefix(pref)?.strip_suffix(suff)
+}
+
+fn parse_path(path: &str) -> &str {
+	let path = strip_prefix_suffix(path, "\x1b[35;1m", "\x1b[0m").unwrap_or(path);
+	path
+}
+
+fn parse_drv(drv: &str) -> &str {
+	let drv = parse_path(drv);
+	if let Some(pkg) = drv.strip_prefix("/nix/store/") {
+		let mut it = pkg.splitn(2, '-');
+		it.next();
+		if let Some(pkg) = it.next() {
+			return pkg;
+		}
+	}
+	drv
+}
+fn parse_host(host: &str) -> &str {
+	if host.is_empty() || host == "local" {
+		return "local";
+	}
+	// https/ssh is the default
+	host.strip_prefix("https://").unwrap_or(host)
+}
+
+impl ActivityType {
+	fn name(&self) -> &'static str {
+		match self {
+			ActivityType::Unknown => "nix",
+			ActivityType::CopyPath => "nix::copy-path",
+			ActivityType::FileTransfer => "nix::file-transfer",
+			ActivityType::Realise => "nix::realise",
+			ActivityType::CopyPaths => "nix::copy-paths",
+			ActivityType::Builds => "nix::builds",
+			ActivityType::Build => "nix::build",
+			ActivityType::OptimiseStore => "nix::optimise-store",
+			ActivityType::VerifyPaths => "nix::verify-paths",
+			ActivityType::Substitute => "nix::substitute",
+			ActivityType::QueryPathInfo => "nix::query-path-info",
+			ActivityType::PostBuildHook => "nix::post-build-hook",
+			ActivityType::BuildWaiting => "nix::build-waiting",
+			ActivityType::FetchTree => "nix::fetch-tree",
+		}
+	}
+	fn format(
+		&self,
+		values: &[FieldValue],
+		s: &str,
+		into: impl FnOnce(Arguments<'_>) -> Span,
+	) -> Span {
+		use FieldValue::*;
+		match (self, values) {
+			(ActivityType::QueryPathInfo, [Str(drv), Str(host)]) => {
+				let drv = parse_drv(drv);
+				let host = parse_host(host);
+				debug_span!(target: "nix::query-path-info", "querying", drv, host)
+			}
+			(ActivityType::Substitute, [Str(drv), Str(host)]) => {
+				let drv = parse_drv(drv);
+				let host = parse_host(host);
+				debug_span!(target: "nix::substitute", "substituting", drv, host)
+			}
+			(ActivityType::CopyPath, [Str(drv), Str(from), Str(to)]) => {
+				let drv = parse_drv(drv);
+				let from = parse_host(from);
+				let to = parse_host(to);
+				debug_span!(target: "nix::copy-path", "copying", drv, from, to)
+			}
+			(ActivityType::Build, [Str(drv), Str(host), Int(_), Int(_)]) => {
+				let drv = parse_drv(drv);
+				let host = parse_host(host);
+				info_span!(target: "nix::build", "building", drv, host)
+			}
+			(ActivityType::FileTransfer, [Str(file)]) => {
+				info_span!(target: "nix::file-transfer", "downloading", file)
+			}
+			(ActivityType::Realise, []) => {
+				debug_span!(target: "nix::realise", "realising")
+			}
+			(ActivityType::CopyPaths, []) => {
+				debug_span!(target: "nix::copy-paths", "copying paths")
+			}
+			(ActivityType::Unknown, [])
+				if s.starts_with("copying \"") && s.ends_with("\" to the store") =>
+			{
+				let tree = s
+					.trim_start_matches("copying \"")
+					.trim_end_matches("\" to the store");
+				debug_span!(target: "nix::trees", "copying", tree)
+			}
+			(ActivityType::Unknown, [])
+				if s.starts_with("copying '") && s.ends_with("' to the store") =>
+			{
+				let tree = s
+					.trim_start_matches("copying '")
+					.trim_end_matches("' to the store");
+				debug_span!(target: "nix::trees", "copying", tree)
+			}
+			(ActivityType::Unknown, []) if s.starts_with("hashing '") && s.ends_with("'") => {
+				let tree = s.trim_start_matches("hashing '").trim_end_matches("'");
+				debug_span!(target: "nix::trees", "hashing", tree)
+			}
+			(ActivityType::Unknown, []) if s.starts_with("connecting to '") && s.ends_with("'") => {
+				let host = s
+					.trim_start_matches("connecting to '")
+					.trim_end_matches("'");
+				debug_span!(target: "nix::remote", "connecting", host)
+			}
+			(ActivityType::Unknown, [])
+				if s.starts_with("copying outputs from '") && s.ends_with("'") =>
+			{
+				let host = s
+					.trim_start_matches("copying outputs from '")
+					.trim_end_matches("'");
+				debug_span!(target: "nix::remote", "copying outputs", host)
+			}
+			(ActivityType::Unknown, [])
+				if s.starts_with("copying dependencies to '") && s.ends_with("'") =>
+			{
+				let host = s
+					.trim_start_matches("copying dependencies to '")
+					.trim_end_matches("'");
+				debug_span!(target: "nix::remote", "copying dependencies", host)
+			}
+			(ActivityType::Unknown, [])
+				if s.starts_with("waiting for the upload lock to '") && s.ends_with("'") =>
+			{
+				let host = s
+					.trim_start_matches("waiting for the upload lock to '")
+					.trim_end_matches("'");
+				debug_span!(target: "nix::remote", "waiting for upload lock", host)
+			}
+			(ActivityType::BuildWaiting, [])
+				if s.starts_with("waiting for a machine to build '") && s.ends_with("'") =>
+			{
+				let drv = parse_drv(
+					s.trim_start_matches("waiting for a machine to build '")
+						.trim_end_matches("'"),
+				);
+				debug_span!(target: "nix::build-waiting", "waiting for available builder", drv)
+			}
+			(ActivityType::Unknown, []) if s == "querying info about missing paths" => {
+				debug_span!(target: "nix::remote", "querying")
+			}
+			_ => into(format_args!("{}({values:?})", self.name())),
+		}
+	}
+	fn from_int(v: u32) -> Self {
+		match v {
+			0 => Self::Unknown,
+			100 => Self::CopyPath,
+			101 => Self::FileTransfer,
+			102 => Self::Realise,
+			103 => Self::CopyPaths,
+			104 => Self::Builds,
+			105 => Self::Build,
+			106 => Self::OptimiseStore,
+			107 => Self::VerifyPaths,
+			108 => Self::Substitute,
+			109 => Self::QueryPathInfo,
+			110 => Self::PostBuildHook,
+			111 => Self::BuildWaiting,
+			112 => Self::FetchTree,
+			_ => {
+				warn!("unknown nix action: {v}");
+				Self::Unknown
+			}
+		}
+	}
+}
+
+#[derive(Debug)]
+enum ResultType {
+	FileLinked = 100,
+	BuildLogLine = 101,
+	UntrustedPath = 102,
+	CorruptedPath = 103,
+	SetPhase = 104,
+	Progress = 105,
+	SetExpected = 106,
+	PostBuildLogLine = 107,
+	FetchStatus = 108,
+
+	Unknown = 999,
+}
+impl ResultType {
+	fn from_int(v: u32) -> Self {
+		match v {
+			100 => Self::FileLinked,
+			101 => Self::BuildLogLine,
+			102 => Self::UntrustedPath,
+			103 => Self::CorruptedPath,
+			104 => Self::SetPhase,
+			105 => Self::Progress,
+			106 => Self::SetExpected,
+			107 => Self::PostBuildLogLine,
+			108 => Self::FetchStatus,
+
+			_ => {
+				warn!("unknown nix result: {v}");
+				Self::Unknown
+			}
+		}
+	}
+}
+#[derive(Clone, Copy)]
+enum Verbosity {
+	Error,
+	Warn,
+	Notice,
+	Info,
+	Talkative,
+	Chatty,
+	Debug,
+	Vomit,
+}
+impl Into<tracing::Level> for Verbosity {
+	fn into(self) -> tracing::Level {
+		match self {
+			Verbosity::Error => Level::ERROR,
+			Verbosity::Warn => Level::WARN,
+			Verbosity::Notice => Level::WARN,
+			Verbosity::Info => Level::INFO,
+			Verbosity::Talkative => Level::DEBUG,
+			Verbosity::Chatty => Level::DEBUG,
+			Verbosity::Debug => Level::DEBUG,
+			Verbosity::Vomit => Level::TRACE,
+		}
+	}
+}
+impl Verbosity {
+	fn from_int(u: u32) -> Self {
+		[
+			Self::Error,
+			Self::Warn,
+			Self::Notice,
+			Self::Info,
+			Self::Talkative,
+			Self::Chatty,
+			Self::Debug,
+			Self::Vomit,
+		]
+		.get(u as usize)
+		.cloned()
+		.unwrap_or_else(|| {
+			warn!("unknown log level: {u}");
+			Verbosity::Vomit
+		})
+	}
+}
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy)]
+enum MetadataKind {
+	Span,
+	Event,
+}
+// impl MetadataKind {
+// 	fn kind(&self) -> Kind {
+// 		match self {
+// 			MetadataKind::Span => Kind::SPAN,
+// 			MetadataKind::Event => Kind::EVENT,
+// 		}
+// 	}
+// }
+
+#[derive(Hash, PartialEq, Eq)]
+struct ForeignMetadataInfo {
+	target: &'static str,
+	level: Level,
+	kind: MetadataKind,
+	name: &'static str,
+	module: Option<&'static str>,
+	file: Option<&'static str>,
+	line: Option<u32>,
+	names: &'static [&'static str],
+}
+
+struct FakeCallsite;
+impl tracing::callsite::Callsite for FakeCallsite {
+	fn set_interest(&self, interest: tracing::subscriber::Interest) {
+		unreachable!()
+	}
+
+	fn metadata(&self) -> &Metadata<'_> {
+		unreachable!()
+	}
+}
+const FAKE_CALLSITE: FakeCallsite = FakeCallsite;
+
+#[cfg(false)]
+#[derive(Default)]
+struct ForeignSpanData {
+	interned: HashSet<&'static str>,
+	metadatas: HashMap<ForeignMetadataInfo, &'static Metadata<'static>>,
+}
+#[cfg(false)]
+impl ForeignSpanData {
+	fn intern(&mut self, s: &str) -> &'static str {
+		if let Some(v) = self.interned.get(s) {
+			return *v;
+		}
+		let leaked: Box<str> = s.into();
+		let leaked = Box::leak(leaked);
+		self.interned.insert(leaked);
+		return leaked;
+	}
+	fn alloc_metadata<'t>(
+		&'t mut self,
+		target: &'static str,
+		level: Level,
+		kind: MetadataKind,
+		name: &'static str,
+		module: Option<&'static str>,
+		file: Option<&'static str>,
+		line: Option<u32>,
+		names: &'static [&'static str],
+	) -> &'static Metadata<'static> {
+		let info = ForeignMetadataInfo {
+			target,
+			level,
+			kind,
+			name,
+			module,
+			file,
+			line,
+			names,
+		};
+		if let Some(v) = self.metadatas.get(&info) {
+			return *v;
+		}
+		let fake = FakeCallsite;
+		let metadata = Box::leak::<'static>(Box::new(Metadata::new(
+			name,
+			target,
+			level,
+			file,
+			line,
+			module,
+			FieldSet::new(names, tracing::callsite::Identifier(&FAKE_CALLSITE)),
+			kind.kind(),
+		)));
+
+		let meta_raw = &raw const *metadata;
+		let fields_raw = &raw const *metadata.fields();
+
+		// SAFETY: FieldSet struct should be inside of metadata struct... Which we assume here, but do not test
+		// FIXME: Safety comment above might be invalidated at any time, this should actually be covered by unit test (or, better: runtime assertion... Somehow.)
+		let fields_offset = unsafe { fields_raw.cast::<u8>().offset_from(meta_raw.cast()) };
+		let field_set = unsafe {
+			((&raw mut *metadata).cast::<()>())
+				.byte_offset(fields_offset)
+				.cast::<FieldSet>()
+		};
+		// FIXME: metadata borrow here invalidates our &mut borrow of 'static Metadata, and 'static FieldSet so this construction should be replaced with raw pointers or idk.
+		// Something should be better done inside of tracing crate itself, someting like interior mutability.
+		let callsite = Box::leak(Box::new(tracing::callsite::DefaultCallsite::new(metadata)));
+		unsafe { *field_set = FieldSet::new(names, tracing::callsite::Identifier(callsite)) };
+
+		tracing::callsite::register(&*callsite);
+
+		self.metadatas.insert(info, metadata);
+		return metadata;
+	}
+}
+
+#[cfg(false)]
+static FOREIGN_SPAN_DATA: LazyLock<Mutex<ForeignSpanData>> =
+	LazyLock::new(|| Mutex::new(ForeignSpanData::default()));
+static NIX_SPAN_MAPPING: LazyLock<Mutex<HashMap<u64, Span>>> =
+	LazyLock::new(|| Mutex::new(HashMap::new()));
+
+#[derive(Debug)]
+enum FieldValue {
+	Int(i32),
+	Str(String),
+}
+
+struct StartActivityBuilder {
+	activity_id: u64,
+	verbosity: Verbosity,
+	typ: ActivityType,
+	fields: Vec<FieldValue>,
+}
+impl StartActivityBuilder {
+	fn add_int_field(&mut self, i: i32) {
+		self.fields.push(FieldValue::Int(i));
+	}
+	fn add_string_field(&mut self, v: &str) {
+		self.fields.push(FieldValue::Str(v.to_owned()));
+	}
+	fn emit(&mut self, parent: u64, s: &str) {
+		let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");
+
+		let parent = mapping.get(&parent);
+
+		// 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 span = {
+			let _in_parent = parent.map(|p| p.enter());
+			let level: Level = self.verbosity.into();
+			if level == Level::ERROR {
+				self.typ
+					.format(&self.fields, s, |v| error_span!("action", v))
+			} else if level == Level::WARN {
+				self.typ
+					.format(&self.fields, s, |v| warn_span!("action", v))
+			} else if level == Level::INFO {
+				self.typ
+					.format(&self.fields, s, |v| info_span!("action", v))
+			} else if level == Level::DEBUG {
+				self.typ
+					.format(&self.fields, s, |v| debug_span!("action", v))
+			} else {
+				self.typ
+					.format(&self.fields, s, |v| trace_span!("action", v))
+			}
+		};
+		if !s.trim().is_empty() {
+			span.pb_set_message(s);
+			let _e = span.enter();
+			let level: Level = self.verbosity.into();
+			if level == Level::ERROR {
+				error!(target: "nix", "{}", s)
+			} else if level == Level::WARN {
+				warn!(target: "nix", "{}", s)
+			} else if level == Level::INFO {
+				info!(target: "nix", "{}", s)
+			} else if level == Level::DEBUG {
+				debug!(target: "nix", "{}", s)
+			} else {
+				trace!(target: "nix", "{}", s)
+			}
+		} else {
+			span.pb_start();
+		}
+		mapping.insert(self.activity_id, span);
+	}
+	fn emit_result(&mut self, ty: u32) {
+		let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");
+
+		let Some(parent) = mapping.get(&self.activity_id) else {
+			panic!("unexpected result for dead parent");
+		};
+
+		let _in_parent = parent.enter();
+		let res = ResultType::from_int(ty);
+
+		use FieldValue::*;
+		match (&res, self.fields.as_slice()) {
+			// ResultType::FileLinked => todo!(),
+			(ResultType::BuildLogLine, [Str(s)]) => {
+				info!("{s:?}");
+			}
+			// ResultType::UntrustedPath => todo!(),
+			// ResultType::CorruptedPath => todo!(),
+			// ResultType::SetPhase => todo!(),
+			(ResultType::SetExpected, [Int(act_ty), Int(_expected)]) => {
+				let _act_ty = ActivityType::from_int(*act_ty as u32);
+			}
+			(ResultType::SetPhase, [Str(phase)]) => {
+				// parent.pb_set_message(phase);
+				debug!(target: "nix::phase", phase)
+			}
+			(ResultType::Progress, [Int(done), Int(expected), Int(_), Int(_)]) => {
+				parent.pb_set_length(*expected as u64);
+				parent.pb_set_position(*done as u64);
+			}
+			_ => warn!("unknown progress report: {:?}({:?})", &res, &self.fields),
+		}
+	}
+}
+fn new_start_activity(activity_id: u64, lvl: u32, typ: u32) -> Box<StartActivityBuilder> {
+	Box::new(StartActivityBuilder {
+		activity_id,
+		verbosity: Verbosity::from_int(lvl),
+		typ: ActivityType::from_int(typ),
+		fields: vec![],
+	})
+}
+
+fn emit_warn(v: &str) {
+	warn!(target: "nix::eval", "{v}")
+}
+fn emit_stop(v: u64) {
+	let mut mapping = NIX_SPAN_MAPPING.lock().expect("not poisoned");
+	mapping.remove(&v);
+}
+fn emit_log(lvl: u32, v: &str) {
+	let verbosity = Verbosity::from_int(lvl);
+	let level: Level = verbosity.into();
+	if level == Level::ERROR {
+		error!(target: "nix", "{v}")
+	} else if level == Level::WARN {
+		warn!(target: "nix", "{v}")
+	} else if level == Level::INFO {
+		info!(target: "nix", "{v}")
+	} else if level == Level::DEBUG {
+		debug!(target: "nix", "{v}")
+	} else {
+		trace!(target: "nix", "{v}")
+	}
+}
+
+// fn start_activity(act: u64, lvl: u32, act_ty: u32, s: &str, parent: u32) {
+// 	tracing::Span::new(meta, values)
+// }
+
+#[cxx::bridge]
+pub mod nix_logging_cxx {
+	extern "Rust" {
+		type StartActivityBuilder;
+		fn new_start_activity(activity_id: u64, lvl: u32, typ: u32) -> Box<StartActivityBuilder>;
+		fn add_int_field(&mut self, i: i32);
+		fn add_string_field(&mut self, v: &str);
+		fn emit(&mut self, parent: u64, s: &str);
+		fn emit_result(&mut self, ty: u32);
+
+		fn emit_warn(v: &str);
+		fn emit_stop(id: u64);
+		fn emit_log(lvl: u32, v: &str);
+	}
+	unsafe extern "C++" {
+		include!("nix-eval/src/logging.hh");
+
+		fn apply_tracing_logger();
+	}
+}
modifiedcrates/nix-eval/src/macros.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/macros.rs
+++ b/crates/nix-eval/src/macros.rs
@@ -1,171 +1,26 @@
-use serde::Serialize;
-
-use crate::{NixSession, Value};
-
-#[derive(Clone)]
-pub struct NixExprBuilder {
-	pub(crate) out: String,
-	used_fields: Vec<Value>,
-}
-pub trait AttrSetValue {
-	fn to_builder(self) -> NixExprBuilder;
-}
-trait Primitive {}
-
-macro_rules! impl_primitive {
-	($($t:ty),+) => {
-			$(
-				impl Primitive for $t {}
-			)+
-	};
-}
-impl_primitive!(String, bool, &'static str);
-
-impl<T> AttrSetValue for T
-where
-	// Limited by Primitive trait to avoid orphan rules violation with Vec<T: AttrSetValue>
-	T: Serialize + Primitive,
-{
-	fn to_builder(self) -> NixExprBuilder {
-		let serialized = nixlike::serialize(self).expect("invalid value for apply");
-		NixExprBuilder {
-			out: serialized.trim_end().to_owned(),
-			used_fields: Vec::new(),
-		}
-	}
-}
-impl AttrSetValue for Value {
-	fn to_builder(self) -> NixExprBuilder {
-		NixExprBuilder {
-			out: format!("sess_field_{}", self.session_field_id()),
-			used_fields: vec![self],
-		}
-	}
-}
-impl<T> AttrSetValue for Vec<T>
-where
-	T: AttrSetValue,
-{
-	fn to_builder(self) -> NixExprBuilder {
-		let mut builder = NixExprBuilder::list();
-		for v in self {
-			builder.list_value(NixExprBuilder::attrset_value(v));
-		}
-		builder.list_end();
-		builder
-	}
-}
-
-impl NixExprBuilder {
-	pub fn object() -> Self {
-		NixExprBuilder {
-			out: "{ ".to_owned(),
-			used_fields: Vec::new(),
-		}
-	}
-	pub fn list() -> Self {
-		NixExprBuilder {
-			out: "[".to_owned(),
-			used_fields: Vec::new(),
-		}
-	}
-	pub fn string(s: &str) -> Self {
-		NixExprBuilder {
-			out: nixlike::serialize(s)
-				.expect("no problems with serializing_string")
-				.trim_end()
-				.to_owned(),
-			used_fields: Vec::new(),
-		}
-	}
-	pub fn attrset_value(v: impl AttrSetValue) -> Self {
-		v.to_builder()
-	}
-	pub fn serialized(v: impl Serialize) -> Self {
-		let serialized = nixlike::serialize(v).expect("invalid value for apply");
-		Self {
-			out: serialized.trim_end().to_owned(),
-			used_fields: Vec::new(),
-		}
-	}
-	pub fn value(f: Value) -> Self {
-		Self {
-			out: format!("sess_field_{}", f.session_field_id()),
-			used_fields: vec![f],
-		}
-	}
-	pub fn end_obj(&mut self) {
-		self.out.push('}');
-	}
-	pub fn obj_key(&mut self, name: Self, value: Self) {
-		self.out.push_str(r#""${"#);
-		self.extend(name);
-		self.out.push_str(r#"}" = "#);
-		self.extend(value);
-		self.out.push_str("; ");
-	}
-	pub fn list_value(&mut self, value: Self) {
-		self.extend(value);
-		self.out.push(' ');
-	}
-	pub fn list_end(&mut self) {
-		self.out.push(']');
-	}
-
-	pub fn extend(&mut self, e: Self) {
-		self.out.push_str(&e.out);
-		self.used_fields.extend(e.used_fields);
-	}
-
-	#[allow(dead_code)]
-	pub fn session(&self) -> NixSession {
-		let mut session = None;
-		for ele in &self.used_fields {
-			if session.is_none() {
-				session = Some(ele.session());
-				continue;
-			}
-			let session = session.as_ref().expect("checked");
-			let ele_sess = ele.session();
-			assert!(
-				NixSession::ptr_eq(session, &ele_sess),
-				"can't mix fields from different session"
-			);
-		}
-		session.expect("expr without fields used")
-	}
-	#[allow(dead_code)]
-	pub fn index_attr(&mut self, s: &str) {
-		let escaped = nixlike::serialize(s).expect("string");
-		self.out.push('.');
-		self.out.push_str(escaped.trim_end());
-	}
-}
-
 #[macro_export]
 macro_rules! nix_expr_inner {
 	//(@munch_object FIXME: value should be arbitrary nix_expr_inner input... Time to write proc-macro?
 	(@obj($o:ident) $field:ident$(, $($tt:tt)*)?) => {{
-		$o.obj_key(
-			NixExprBuilder::string(stringify!($field)),
-			NixExprBuilder::attrset_value($field),
+		$o.insert(
+			stringify!($field),
+			$crate::Value::from($field),
 		);
 		$(nix_expr_inner!(@obj($o) $($tt)*);)?
 	}};
 	(@obj($o:ident) $field:ident: $v:expr$(, $($tt:tt)*)?) => {{
-		$o.obj_key(
-			NixExprBuilder::string(stringify!($field)),
-			NixExprBuilder::attrset_value($v),
+		$o.insert(
+			stringify!($field),
+			$crate::Value::from($v),
 		);
 		$(nix_expr_inner!(@obj($o) $($tt)*);)?
 	}};
 	(@obj($o:ident)) => {{}};
 	(Obj { $($tt:tt)* }) => {{
-		use $crate::{macros::NixExprBuilder, nix_expr_inner};
-		let mut out = NixExprBuilder::object();
+		use $crate::{nix_expr_inner};
+		let mut out = std::collections::hash_map::HashMap::new();
 		nix_expr_inner!(@obj(out) $($tt)*);
-		out.end_obj();
-		out
+		Value::new_attrs(out)?
 	}};
 	(@field($o:ident) . $var:ident $($tt:tt)*) => {{
 		$o.index_attr(stringify!($var));
@@ -185,10 +40,10 @@
 	};
 	(@field($o:ident)) => {};
 	($field:ident $($tt:tt)*) => {{
-		use $crate::{macros::NixExprBuilder, nix_expr_inner};
+		use $crate::{nix_expr_inner};
 		// might be used if indexed
 		#[allow(unused_mut)]
-		let mut out = NixExprBuilder::value($field.clone());
+		let mut out = $field.clone();
 		nix_expr_inner!(@field(out) $($tt)*);
 		out
 	}};
@@ -197,8 +52,7 @@
 		NixExprBuilder::string($v)
 	}};
 	({$v:expr}) => {{
-		use $crate::macros::NixExprBuilder;
-		NixExprBuilder::serialized(&$v)
+		$crate::Value::serialized(&$v)?
 	}}
 }
 #[macro_export]
@@ -212,40 +66,25 @@
 
 #[macro_export]
 macro_rules! nix_go {
-	(@o($o:ident) . $var:ident $($tt:tt)*) => {{
-		$o.push(Index::attr(stringify!($var)));
-		nix_go!(@o($o) $($tt)*);
-	}};
-	(@o($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
-		$o.push(Index::attr(&$v));
-		nix_go!(@o($o) $($tt)*);
+	(@o($o:expr) . $var:ident $($tt:tt)*) => {{
+		nix_go!(@o($o.get_field(stringify!($var))?) $($tt)*)
 	}};
-	(@o($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
-		$o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
-		nix_go!(@o($o) $($tt)*);
+	(@o($o:expr) [ $v:expr ] $($tt:tt)*) => {{
+		nix_go!(@o($o.get_field($v)?) $($tt)*)
 	}};
-	(@o($o:ident) ($($var:tt)*) $($tt:tt)*) => {
-		$o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
-		nix_go!(@o($o) $($tt)*);
-	};
-	(@o($o:ident) | $($var:tt)*) => {
-		$o.push(Index::Pipe($crate::nix_expr_inner!($($var)+)));
-	};
-	(@o($o:ident) + $($var:tt)*) => {
-		$o.push(Index::Merge($crate::nix_expr_inner!($($var)+)));
+	(@o($o:expr) ($($var:tt)*) $($tt:tt)*) => {
+		nix_go!(@o($o.call($crate::nix_expr_inner!($($var)+))?) $($tt)*)
 	};
-	(@o($o:ident)) => {};
+	(@o($o:expr)) => {$o};
 	($field:ident $($tt:tt)+) => {{
-		use $crate::{nix_go, Index};
-		let field = $field.clone();
-		let mut out = vec![];
-		nix_go!(@o(out) $($tt)*);
-		field.select(out).await?
+		use $crate::nix_go;
+		let out = $field.clone();
+		nix_go!(@o(out) $($tt)*)
 	}}
 }
 #[macro_export]
 macro_rules! nix_go_json {
 	($($tt:tt)*) => {{
-		$crate::nix_go!($($tt)*).as_json().await?
+		$crate::nix_go!($($tt)*).as_json()?
 	}};
 }
deletedcrates/nix-eval/src/pool.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/pool.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-use std::{
-	ffi::OsString,
-	sync::{Arc, OnceLock},
-};
-
-use r2d2::Pool;
-
-use crate::{Error, NixSession, Result, session::NixSessionInner};
-
-pub struct NixSessionPool(Pool<NixSessionPoolInner>);
-impl NixSessionPool {
-	pub async fn new(
-		flake: OsString,
-		nix_args: Vec<OsString>,
-		nix_system: String,
-		fail_fast: bool,
-	) -> Result<Self> {
-		let inner = tokio::task::block_in_place(|| {
-			r2d2::Builder::<NixSessionPoolInner>::new()
-				.min_idle(Some(0))
-				.build(NixSessionPoolInner {
-					flake,
-					nix_args,
-					nix_system,
-					fail_fast,
-				})
-		})?;
-		Ok(Self(inner))
-	}
-	pub async fn get(&self) -> Result<NixSession> {
-		let v = tokio::task::block_in_place(|| self.0.get())?;
-		Ok(NixSession(Arc::new(tokio::sync::Mutex::new(v))))
-	}
-}
-
-pub(crate) struct NixSessionPoolInner {
-	flake: OsString,
-	nix_args: Vec<OsString>,
-	fail_fast: bool,
-	pub(crate) nix_system: String,
-}
-
-impl r2d2::ManageConnection for NixSessionPoolInner {
-	type Connection = NixSessionInner;
-	type Error = Error;
-	fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
-		let _v = TOKIO_RUNTIME
-			.get()
-			.expect("missed tokio runtime init!")
-			.enter();
-		futures::executor::block_on(NixSessionInner::new(
-			self.flake.as_os_str(),
-			self.nix_args.iter().map(OsString::as_os_str),
-			self.nix_system.clone(),
-			self.fail_fast,
-		))
-	}
-
-	fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
-		let _v = TOKIO_RUNTIME
-			.get()
-			.expect("missed tokio runtime init!")
-			.enter();
-		let res = futures::executor::block_on(conn.execute_expression_number("2 + 2"))?;
-		if res != 4 {
-			// just in case, should fail much earlier
-			return Err(Error::SessionInit("misbehaving session"));
-		};
-		Ok(())
-	}
-
-	fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
-		false
-	}
-}
-pub static TOKIO_RUNTIME: OnceLock<tokio::runtime::Handle> = OnceLock::new();
deletedcrates/nix-eval/src/session.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/session.rs
+++ /dev/null
@@ -1,450 +0,0 @@
-use std::{ffi::OsStr, num::ParseIntError, process::Stdio, sync::Arc};
-
-use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
-use futures::StreamExt;
-use itertools::Itertools as _;
-use serde::{Deserialize, de::DeserializeOwned};
-use thiserror::Error;
-use tokio::{
-	io::AsyncWriteExt,
-	process::{ChildStderr, ChildStdin, ChildStdout, Command},
-	select,
-	sync::{Mutex, mpsc, oneshot},
-};
-use tokio_util::codec::{FramedRead, LinesCodec};
-use tracing::{Level, debug, error, warn};
-
-#[derive(Error, Debug, Clone)]
-pub enum Error {
-	#[error("failed to create nix repl session: {0}")]
-	SessionInit(&'static str),
-	#[error("unexpected end of output, nix crashed?")]
-	MissingDelimiter,
-
-	#[error("expression did'nt produce any output")]
-	ExpectedOutput,
-	#[error("expression produced output, which is unexpected")]
-	UnexpectedOutput,
-
-	#[error("unexpected expression output type")]
-	InvalidType,
-
-	#[error("failed to build attr {attribute}:\n{error}")]
-	BuildFailed { attribute: String, error: String },
-
-	#[error("output: {0}")]
-	Json(Arc<serde_json::Error>),
-	// int outputs are too specific, and should not be used,
-	// thus error is ok to be not informative.
-	#[error("int output: {0}")]
-	Int(ParseIntError),
-	#[error("pool: {0}")]
-	Pool(Arc<r2d2::Error>),
-	#[error("io: {0}")]
-	Io(Arc<std::io::Error>),
-
-	// TODO: Should be done by wrapper/in different type.
-	#[error("at {0}: {1}")]
-	InContext(String, Box<Self>),
-
-	#[error("error: {0}")]
-	NixError(String),
-}
-impl From<r2d2::Error> for Error {
-	fn from(value: r2d2::Error) -> Self {
-		Self::Pool(Arc::new(value))
-	}
-}
-impl From<std::io::Error> for Error {
-	fn from(value: std::io::Error) -> Self {
-		Self::Io(Arc::new(value))
-	}
-}
-impl From<serde_json::Error> for Error {
-	fn from(value: serde_json::Error) -> Self {
-		Self::Json(Arc::new(value))
-	}
-}
-impl Error {
-	pub(crate) fn context(self, context: String) -> Self {
-		Self::InContext(context, Box::new(self))
-	}
-}
-pub type Result<T, E = Error> = std::result::Result<T, E>;
-
-enum OutputLine {
-	Out(String),
-	Err(String),
-}
-struct OutputHandler {
-	rx: mpsc::Receiver<OutputLine>,
-	_cancel_handle: oneshot::Receiver<()>,
-}
-impl OutputHandler {
-	fn new(out: ChildStdout, err: ChildStderr) -> Self {
-		let mut out = FramedRead::new(out, LinesCodec::new());
-		let mut err = FramedRead::new(err, LinesCodec::new());
-		let (tx, rx) = mpsc::channel(20);
-		let (mut cancelled, _cancel_handle) = oneshot::channel();
-		tokio::spawn(async move {
-			loop {
-				select! {
-					// We should receive errors earlier than synchronization
-					biased;
-					e = err.next() => {
-						let Some(Ok(e)) = e else {
-							if e.is_some() {
-								error!("bad repl stderr: {e:?}");
-							}
-							continue;
-						};
-						let _ = tx.send(OutputLine::Err(e)).await;
-					}
-					o = out.next() => {
-						let Some(Ok(o)) = o else {
-							if o.is_some() {
-								error!("bad repl stdout: {o:?}");
-							}
-							continue;
-						};
-						let _ = tx.send(OutputLine::Out(o)).await;
-					}
-					// Reader doesn't care about stdout, as this is cancelled.
-					// Error still might be useful, to process leftover span closures?
-					_ = cancelled.closed() => {
-						break;
-					}
-				}
-			}
-		});
-		Self { rx, _cancel_handle }
-	}
-	async fn next(&mut self) -> Option<OutputLine> {
-		self.rx.recv().await
-	}
-}
-
-#[must_use]
-struct ErrorCollector<'i, H> {
-	collected: Vec<String>,
-	inner: &'i mut H,
-}
-impl<'i, H> ErrorCollector<'i, H> {
-	fn new(inner: &'i mut H) -> Self {
-		Self {
-			collected: vec![],
-			inner,
-		}
-	}
-}
-impl<H> ErrorCollector<'_, H> {
-	fn handle_line_inner(&mut self, msg: &str) -> bool {
-		let Some(msg) = msg.strip_prefix("@nix ") else {
-			return false;
-		};
-		#[derive(Deserialize)]
-		struct ErrorAction {
-			action: String,
-			level: u32,
-			msg: String,
-		}
-		let Ok(act) = serde_json::from_str::<ErrorAction>(msg) else {
-			return false;
-		};
-		if act.action != "msg" || act.level != 0 {
-			return false;
-		}
-		self.collected.push(act.msg);
-		true
-	}
-	fn finish(self) -> Result<()> {
-		// fn dedent(s: String) -> String {
-		// 	s.split('\n').filter(|s| !s.trim().is_empty()).map(|v| v.)
-		// }
-		if !self.collected.is_empty() {
-			return Err(Error::NixError(
-				self.collected
-					.iter()
-					.map(|v| {
-						if let Some(f) = v.strip_prefix("\u{1b}[31;1merror:\u{1b}[0m ") {
-							let v = unindent::unindent(f.trim_start());
-							v.trim().to_owned()
-						} else {
-							v.to_owned()
-						}
-					})
-					.join("\n")
-					.to_string(),
-			));
-		}
-		Ok(())
-	}
-	fn flush(self) {
-		for line in self.collected {
-			warn!("{line}");
-		}
-	}
-}
-impl<H: Handler> Handler for ErrorCollector<'_, H> {
-	fn handle_line(&mut self, e: &str) {
-		if self.handle_line_inner(e) {
-			return;
-		}
-		self.inner.handle_line(e)
-	}
-}
-
-pub struct NixSessionInner {
-	full_delimiter: String,
-	nix_handler: ClonableHandler<NixHandler>,
-	out: OutputHandler,
-	stdin: ChildStdin,
-	string_wrapping: (String, String),
-	number_wrapping: (String, String),
-
-	executing_command: Arc<Mutex<()>>,
-
-	next_id: u32,
-	pub(crate) free_list: Vec<u32>,
-
-	pub nix_system: String,
-}
-
-/// Discover inter-message repl delimiter
-const REPL_DELIMITER: &str = "\"FLEET_MAGIC_REPL_DELIMITER\"";
-/// Discover formatting around strings
-const TRAIN_STRING: &str = "\"TRAIN_STRING\"";
-/// Discover formatting around numbers
-const TRAIN_NUMBER: &str = "13141516";
-// Other types of formatting are not discovered, because they are not used, JSON serialization is used instead
-// Techically, number training is also not required, because numbers can be converted to string too...
-// Eh, I'll remove it later.
-
-impl NixSessionInner {
-	pub(crate) async fn new(
-		flake: &OsStr,
-		extra_args: impl IntoIterator<Item = &OsStr>,
-		nix_system: String,
-		fail_fast: bool,
-	) -> Result<Self> {
-		let mut cmd = Command::new("nix");
-		cmd.arg("repl")
-			.args(["--option", "pure-eval", "true"])
-			.arg(flake)
-			.arg("--log-format")
-			.arg("internal-json");
-		if !fail_fast {
-			cmd.arg("--keep-going");
-		}
-		for arg in extra_args {
-			cmd.arg(arg);
-		}
-		cmd.stdin(Stdio::piped());
-		cmd.stdout(Stdio::piped());
-		cmd.stderr(Stdio::piped());
-		let cmd = cmd.spawn()?;
-		let stdout = cmd.stdout.unwrap();
-		let stderr = cmd.stderr.unwrap();
-		let mut out = OutputHandler::new(stdout, stderr);
-		let mut stdin = cmd.stdin.unwrap();
-		// Standard repl hello doesn't work with internal-json logger
-		stdin.write_all(REPL_DELIMITER.as_bytes()).await?;
-		stdin.write_all(b"\n").await?;
-		stdin.flush().await?;
-		let nix_handler = NixHandler::default();
-		let mut full_delimiter = None;
-		let mut errors = vec![];
-		while let Some(line) = out.next().await {
-			let line = match line {
-				OutputLine::Out(o) => o,
-				OutputLine::Err(_e) => {
-					// Handle startup errors, but skip repl hello?
-					errors.push(_e);
-					continue;
-				}
-			};
-			if line.contains(REPL_DELIMITER) {
-				debug!("discovered repl delimiter with added colors: {line}");
-				full_delimiter = Some(line.to_owned());
-				break;
-			}
-		}
-		let Some(full_delimiter) = full_delimiter else {
-			for e in errors {
-				error!("{e}");
-			}
-			return Err(Error::SessionInit("failed to discover delimiter"));
-		};
-		let mut res = Self {
-			full_delimiter,
-			nix_handler: ClonableHandler::new(nix_handler),
-			out,
-			stdin,
-			string_wrapping: Default::default(),
-			number_wrapping: Default::default(),
-
-			executing_command: Arc::new(Mutex::new(())),
-
-			next_id: 0,
-			free_list: vec![],
-
-			nix_system,
-		};
-		res.train().await?;
-		Ok(res)
-	}
-	async fn train(&mut self) -> Result<()> {
-		{
-			let full_string = self
-				.execute_expression_raw(TRAIN_STRING, &mut NoopHandler)
-				.await?;
-			let string_offset = full_string.find(TRAIN_STRING).expect("contained");
-			let string_prefix = &full_string[..string_offset];
-			let string_suffix = &full_string[string_offset + TRAIN_STRING.len()..];
-			self.string_wrapping = (string_prefix.to_owned(), string_suffix.to_owned());
-		}
-		{
-			let full_number = self
-				.execute_expression_raw(TRAIN_NUMBER, &mut NoopHandler)
-				.await?;
-			let number_offset = full_number.find(TRAIN_NUMBER).expect("contained");
-			let number_prefix = &full_number[..number_offset];
-			let number_suffix = &full_number[number_offset + TRAIN_NUMBER.len()..];
-			self.number_wrapping = (number_prefix.to_owned(), number_suffix.to_owned());
-		}
-		Ok(())
-	}
-	async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {
-		if tracing::enabled!(Level::DEBUG) && cmd.as_ref() != REPL_DELIMITER.as_bytes() {
-			let cmd_str = String::from_utf8_lossy(cmd.as_ref());
-			tracing::debug!("{cmd_str}");
-		};
-		self.stdin.write_all(cmd.as_ref()).await?;
-		self.stdin.write_all(b"\n").await?;
-		Ok(())
-	}
-	async fn read_until_delimiter(&mut self, err_handler: &mut dyn Handler) -> Result<String> {
-		let mut out = String::new();
-		while let Some(line) = self.out.next().await {
-			let line = match line {
-				OutputLine::Out(out) => out,
-				OutputLine::Err(err) => {
-					err_handler.handle_line(&err);
-					continue;
-				}
-			};
-			if line == self.full_delimiter {
-				return Ok(out);
-			}
-			if !out.is_empty() {
-				out.push('\n');
-			}
-			out.push_str(&line);
-		}
-		Err(Error::MissingDelimiter)
-	}
-	pub(crate) async fn execute_expression_number(
-		&mut self,
-		expr: impl AsRef<[u8]>,
-	) -> Result<u64> {
-		let num = self.number_wrapping.clone();
-		let n = self.execute_expression_wrapping(expr, &num).await?;
-		n.parse::<u64>().map_err(Error::Int)
-	}
-	async fn execute_expression_string(&mut self, expr: impl AsRef<[u8]>) -> Result<String> {
-		// builtins.toJSON escapes some thing in incorrect way, e.g escaped "$" in "\${" is being outputed as "\$",
-		// while this escape should be removed as it is intended for nix itself, not for json output.
-		//
-		// This regex only allows \$ in the beginning of the string, it is easier to implement correctly.
-		// TODO: Add peg parser for nix-produced JSON?..
-		let regex = regex::Regex::new(r#"(?<prefix>[: {,\[]\\")\\\$"#).expect("fixup json");
-
-		let num = self.string_wrapping.clone();
-		let n = self.execute_expression_wrapping(expr, &num).await?;
-		let n = regex.replace_all(&n, "$prefix$$");
-		let str: String = serde_json::from_str(&n)?;
-		Ok(str)
-	}
-	pub(crate) async fn execute_expression_to_json<V: DeserializeOwned>(
-		&mut self,
-		expr: impl AsRef<[u8]>,
-	) -> Result<V> {
-		let mut fexpr = b"builtins.toJSON (".to_vec();
-		fexpr.extend_from_slice(expr.as_ref());
-		fexpr.push(b')');
-
-		Ok(serde_json::from_str(
-			&self.execute_expression_string(fexpr).await?,
-		)?)
-	}
-	async fn execute_expression_wrapping(
-		&mut self,
-		expr: impl AsRef<[u8]>,
-		wrapping: &(String, String),
-	) -> Result<String> {
-		let mut nix_handler = self.nix_handler.clone();
-		let mut collected = ErrorCollector::new(&mut nix_handler);
-		let res = self.execute_expression_raw(expr, &mut collected).await?;
-		if res.is_empty() {
-			collected.finish()?;
-			return Err(Error::ExpectedOutput);
-		} else {
-			collected.flush()
-		};
-		let Some(res) = res.strip_prefix(&wrapping.0) else {
-			return Err(Error::InvalidType);
-		};
-		let Some(res) = res.strip_suffix(&wrapping.1) else {
-			return Err(Error::InvalidType);
-		};
-		Ok(res.to_owned())
-	}
-	async fn execute_expression_empty(&mut self, expr: impl AsRef<[u8]>) -> Result<()> {
-		let mut nix_handler = self.nix_handler.clone();
-		let mut collected = ErrorCollector::new(&mut nix_handler);
-		let v = self.execute_expression_raw(expr, &mut collected).await?;
-		collected.finish()?;
-		if !v.is_empty() {
-			return Err(Error::UnexpectedOutput);
-		}
-		Ok(())
-	}
-	pub(crate) async fn execute_expression_raw(
-		&mut self,
-		expr: impl AsRef<[u8]>,
-		err_handler: &mut dyn Handler,
-	) -> Result<String> {
-		// Prevent two commands from being executed in parallel, messing with each other.
-		let _lock = self.executing_command.clone();
-		let _guard = _lock.lock().await;
-
-		self.send_command(expr).await?;
-		// It will be echoed
-		self.send_command(REPL_DELIMITER).await?;
-		self.read_until_delimiter(err_handler).await
-	}
-	pub(crate) async fn execute_assign(&mut self, expr: impl AsRef<str>) -> Result<u32> {
-		let id = self.allocate_id();
-		self.execute_expression_empty(format!("sess_field_{id} = {}", expr.as_ref()))
-			.await?;
-		Ok(id)
-	}
-
-	/// Id should be immediately used
-	fn allocate_id(&mut self) -> u32 {
-		if let Some(free) = self.free_list.pop() {
-			free
-		} else {
-			let v = self.next_id;
-			self.next_id += 1;
-			v
-		}
-	}
-	// Nix has no way to deallocate variable, yet GC will correct everything not reachable.
-	// async fn free_id(&mut self, id: u32) -> Result<()> {
-	// 	self.execute_expression_empty(format!("sess_field_{id} = null"))
-	// 		.await?;
-	// 	self.free_list.push(id);
-	// 	Ok(())
-	// }
-}
modifiedcrates/nix-eval/src/value.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/value.rs
+++ b/crates/nix-eval/src/value.rs
@@ -3,302 +3,4 @@
 use better_command::NixHandler;
 use serde::{Serialize, de::DeserializeOwned};
 
-use crate::{Error, NixBuildBatch, NixSession, Result, macros::NixExprBuilder, nix_go};
-
-#[derive(Clone)]
-pub enum Index {
-	Var(String),
-	String(String),
-	#[allow(dead_code)]
-	Apply(String),
-	#[allow(dead_code)]
-	Expr(NixExprBuilder),
-	ExprApply(NixExprBuilder),
-	Pipe(NixExprBuilder),
-	Merge(NixExprBuilder),
-}
-impl Index {
-	pub fn var(v: impl AsRef<str>) -> Self {
-		let v = v.as_ref();
-		assert!(
-			!(v.contains('.') | v.contains(' ')),
-			"bad variable name: {v}"
-		);
-		Self::Var(v.to_owned())
-	}
-	pub fn attr(v: impl AsRef<str>) -> Self {
-		Self::String(v.as_ref().to_owned())
-	}
-	#[allow(dead_code)]
-	pub fn apply(v: impl Serialize) -> Self {
-		let serialized = nixlike::serialize(v).expect("invalid value for apply");
-		Self::Apply(serialized.trim_end().to_owned())
-	}
-}
-impl fmt::Display for Index {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		match self {
-			Index::Var(v) => {
-				write!(f, "{v}")
-			}
-			Index::String(k) => {
-				let v = nixlike::format_identifier(k.as_str());
-				write!(f, ".{v}")
-			}
-			Index::Apply(_) => {
-				write!(f, "<apply>(...)")
-			}
-			Index::Expr(e) => {
-				write!(f, "[{}]", e.out)
-			}
-			Index::ExprApply(_) => {
-				write!(f, "<apply>(...)")
-			}
-			Index::Pipe(e) => {
-				write!(f, "<map>({})", e.out)
-			}
-			Index::Merge(e) => {
-				write!(f, "//({})", e.out)
-			}
-		}
-	}
-}
-impl fmt::Debug for Index {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{self}")
-	}
-}
-struct PathDisplay<'i>(&'i [Index]);
-impl fmt::Display for PathDisplay<'_> {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		if !matches!(self.0.first(), Some(Index::Var(_))) {
-			write!(f, "<unknown>")?;
-		}
-		for i in self.0 {
-			write!(f, "{i}")?;
-		}
-		Ok(())
-	}
-}
-struct ValueInner {
-	full_path: Vec<Index>,
-	session: NixSession,
-	value: u32,
-}
-#[derive(Clone)]
-pub struct Value(Arc<ValueInner>);
-impl Value {
-	async fn new(session: NixSession, query: &str) -> Result<Self> {
-		let vid = session.0.lock().await.execute_assign(query).await?;
-		Ok(Self(Arc::new(ValueInner {
-			full_path: vec![],
-			session,
-			value: vid,
-		})))
-	}
-	/// Get a top-level binding.
-	///
-	/// In flake repl session, every output is exposed as top-level binding.
-	pub async fn binding(session: NixSession, query: &str) -> Result<Self> {
-		// TODO: Verify that query is a valid variable name
-		let vid = session.0.lock().await.execute_assign(query).await?;
-		Ok(Self(Arc::new(ValueInner {
-			full_path: vec![Index::Var(query.to_owned())],
-			session,
-			value: vid,
-		})))
-	}
-	pub async fn select(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {
-		let mut used_fields = Vec::new();
-		let name = name.into_iter();
-
-		let mut full_path = self.0.full_path.clone();
-		let mut query = self.sess_field_name();
-		for v in name {
-			full_path.push(v.clone());
-			match v {
-				Index::Var(_) => panic!("var item may only be first"),
-				Index::String(s) => {
-					let escaped =
-						nixlike::serialize(s).expect("strings are always serialized successfully");
-					query.push('.');
-					query.push_str(escaped.trim());
-				}
-				Index::Apply(a) => {
-					// In cases like `a {}.b` first `{}.b` will be evaluated, so `a {}` should be encased in `()`
-					query = format!("({query} {a})");
-				}
-				Index::Expr(e) => {
-					let index = Value::new(self.0.session.clone(), &e.out).await?;
-					used_fields.push(index.clone());
-					query.push('.');
-					let index = format!("${{sess_field_{}}}", index.0.value);
-					query.push_str(&index);
-				}
-				Index::ExprApply(e) => {
-					let index = Value::new(self.0.session.clone(), &e.out).await?;
-					used_fields.push(index.clone());
-					query.push(' ');
-					let index = format!("sess_field_{}", index.0.value);
-					query.push_str(&index);
-					query = format!("({query})");
-				}
-				Index::Pipe(v) => {
-					let index = Value::new(self.0.session.clone(), &v.out).await?;
-					used_fields.push(index.clone());
-					let index = format!("sess_field_{}", index.0.value);
-					query = format!("({index} {query})");
-				}
-				Index::Merge(v) => {
-					let index = Value::new(self.0.session.clone(), &v.out).await?;
-					used_fields.push(index.clone());
-					let index = format!("sess_field_{}", index.0.value);
-					query = format!("({query} // {index})");
-				}
-			}
-		}
-
-		let vid = self
-			.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_assign(&query)
-			.await
-			.map_err(|e| e.context(self.attribute()))?;
-		Ok(Self(Arc::new(ValueInner {
-			full_path,
-			session: self.0.session.clone(),
-			value: vid,
-		})))
-	}
-	pub async fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
-		let query = self.sess_field_name();
-		self.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_to_json(&query)
-			.await
-			.map_err(|e| e.context(self.attribute()))
-	}
-	#[allow(dead_code)]
-	pub async fn has_field(&self, name: &str) -> Result<bool> {
-		let key = nixlike::escape_string(name);
-		let query = format!("{} ? {key}", self.sess_field_name());
-		self.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_to_json(&query)
-			.await
-			.map_err(|e| e.context(self.attribute()))
-	}
-	pub async fn list_fields(&self) -> Result<Vec<String>> {
-		let query = format!("builtins.attrNames {}", self.sess_field_name());
-		self.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_to_json(&query)
-			.await
-			.map_err(|e| e.context(self.attribute()))
-	}
-	pub async fn type_of(&self) -> Result<String> {
-		let query = format!("builtins.typeOf {}", self.sess_field_name());
-		self.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_to_json(&query)
-			.await
-			.map_err(|e| e.context(self.attribute()))
-	}
-	#[allow(dead_code)]
-	pub async fn import(&self) -> Result<Self> {
-		let import = Self::new(self.0.session.clone(), "import").await?;
-		Ok(nix_go!(self | import))
-	}
-	fn sess_field_name(&self) -> String {
-		format!("sess_field_{}", self.0.value)
-	}
-	pub async fn build_maybe_batch(
-		&self,
-		batch: Option<NixBuildBatch>,
-	) -> Result<HashMap<String, PathBuf>> {
-		if let Some(batch) = batch {
-			batch.submit(self.clone()).await
-		} else {
-			self.build().await
-		}
-	}
-	pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
-		let query = format!(":b {}", self.sess_field_name());
-		let vid = self
-			.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_raw(&query, &mut NixHandler::default())
-			.await?;
-		if vid.is_empty() {
-			return Err(Error::BuildFailed {
-				attribute: self.attribute(),
-				error: "build produced no output".to_owned(),
-			});
-		}
-		let Some(vid) = vid.strip_prefix("This derivation produced the following outputs:\n")
-		else {
-			return Err(Error::BuildFailed {
-				attribute: self.attribute(),
-				error: format!("failed to parse output: {vid}"),
-			});
-		};
-		let outputs = vid
-			.split('\n')
-			.filter(|v| !v.is_empty())
-			.map(|v| v.split_once(" -> ").expect("unexpected build output"))
-			.map(|(a, b)| (a.trim_start().to_owned(), PathBuf::from(b)))
-			.collect();
-		Ok(outputs)
-	}
-	/// Weakly convert string-like types (derivation/path/string) to string
-	pub async fn to_string_weak(&self) -> Result<String> {
-		let query = format!("\"${{{}}}\"", self.sess_field_name());
-		let vid: String = self
-			.0
-			.session
-			.0
-			.lock()
-			.await
-			.execute_expression_to_json(&query)
-			.await?;
-		Ok(vid)
-	}
-
-	fn attribute(&self) -> String {
-		PathDisplay(&self.0.full_path).to_string()
-	}
-
-	pub(crate) fn session(&self) -> NixSession {
-		self.0.session.clone()
-	}
-
-	pub(crate) fn session_field_id(&self) -> u32 {
-		self.0.value
-	}
-}
-impl Drop for ValueInner {
-	fn drop(&mut self) {
-		if let Ok(mut lock) = self.session.0.try_lock() {
-			lock.free_list.push(self.value)
-		}
-		// Leaked
-	}
-}
+use crate::{Result, Value, nix_go};
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -10,7 +10,7 @@
 alejandra = { git = "https://github.com/kamadorueda/alejandra" }
 linked-hash-map = "0.5.6"
 peg = "0.8.5"
-ron = "0.10.1"
+ron = "0.11.0"
 serde = "1.0.219"
 serde-transcode = "1.1.1"
 serde_json = "1.0.140"
modifiedcrates/nixlike/src/lib.rsdiffbeforeafterboth
--- a/crates/nixlike/src/lib.rs
+++ b/crates/nixlike/src/lib.rs
@@ -5,6 +5,7 @@
 //! expressions and expect it to work, only basic primitives are supported, and there is no
 //! variables/recursive records, interpolation, e.t.c.
 
+use alejandra::config::Indentation;
 use linked_hash_map::LinkedHashMap;
 use peg::str::LineCol;
 use se_impl::MySerialize;
@@ -196,7 +197,13 @@
 	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");
 }
 pub fn format_nix(value: &String) -> String {
-	let (_, out) = alejandra::format::in_memory("".to_owned(), value.to_owned());
+	let (_, out) = alejandra::format::in_memory(
+		"".to_owned(),
+		value.to_owned(),
+		alejandra::config::Config {
+			indentation: Indentation::TwoSpaces,
+		},
+	);
 	out
 }
 
modifiedcrates/nixlike/src/to_string.rsdiffbeforeafterboth
--- a/crates/nixlike/src/to_string.rs
+++ b/crates/nixlike/src/to_string.rs
@@ -1,3 +1,5 @@
+use alejandra::config::Indentation;
+
 use crate::Value;
 
 pub fn write_identifier(k: &str, out: &mut String) {
@@ -98,6 +100,12 @@
 pub fn write_nix(value: &Value) -> String {
 	let mut out = String::new();
 	write_nix_buf(value, &mut out);
-	let (_, out) = alejandra::format::in_memory("".to_owned(), out);
+	let (_, out) = alejandra::format::in_memory(
+		"".to_owned(),
+		out,
+		alejandra::config::Config {
+			indentation: Indentation::TwoSpaces,
+		},
+	);
 	out
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -15,6 +15,22 @@
         "type": "github"
       }
     },
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1696426674,
+        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
     "flake-parts": {
       "inputs": {
         "nixpkgs-lib": [
@@ -35,8 +51,117 @@
         "type": "github"
       }
     },
+    "flake-parts_2": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1748821116,
+        "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=",
+        "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1",
+        "revCount": 377,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev-49f0870db23e8c1ca0b5259734a02cd9e1e371a1/01972f28-554a-73f8-91f4-d488cc502f08/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1"
+      }
+    },
+    "git-hooks-nix": {
+      "inputs": {
+        "flake-compat": "flake-compat",
+        "gitignore": [
+          "nix"
+        ],
+        "nixpkgs": [
+          "nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1747372754,
+        "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=",
+        "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
+        "revCount": 1026,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1026%2Brev-80479b6ec16fefd9c1db3ea13aeb038c60530f46/0196d79a-1b35-7b8e-a021-c894fb62163d/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/cachix/git-hooks.nix/0.1.941"
+      }
+    },
+    "nix": {
+      "inputs": {
+        "flake-parts": "flake-parts_2",
+        "git-hooks-nix": "git-hooks-nix",
+        "nixpkgs": "nixpkgs",
+        "nixpkgs-23-11": "nixpkgs-23-11",
+        "nixpkgs-regression": "nixpkgs-regression"
+      },
+      "locked": {
+        "lastModified": 1756860322,
+        "narHash": "sha256-mT01CpWVdqSm79L270dSkjdYbdc37r+Hq9vk4GTp7Ao=",
+        "path": "/home/lach/build/nix-src",
+        "type": "path"
+      },
+      "original": {
+        "path": "/home/lach/build/nix-src",
+        "type": "path"
+      }
+    },
     "nixpkgs": {
       "locked": {
+        "lastModified": 1755922037,
+        "narHash": "sha256-wY1+2JPH0ZZC4BQefoZw/k+3+DowFyfOxv17CN/idKs=",
+        "rev": "b1b3291469652d5a2edb0becc4ef0246fff97a7c",
+        "revCount": 808723,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808723%2Brev-b1b3291469652d5a2edb0becc4ef0246fff97a7c/0198daf7-011a-7703-95d7-57146e794342/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2505"
+      }
+    },
+    "nixpkgs-23-11": {
+      "locked": {
+        "lastModified": 1717159533,
+        "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
+        "type": "github"
+      }
+    },
+    "nixpkgs-regression": {
+      "locked": {
+        "lastModified": 1643052045,
+        "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+        "type": "github"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
         "lastModified": 1753320130,
         "narHash": "sha256-KCuv6iYQ0XTVAEJvDLIsk99CJm7fuqIE0/KknyeYPtM=",
         "owner": "nixos",
@@ -55,7 +180,8 @@
       "inputs": {
         "crane": "crane",
         "flake-parts": "flake-parts",
-        "nixpkgs": "nixpkgs",
+        "nix": "nix",
+        "nixpkgs": "nixpkgs_2",
         "rust-overlay": "rust-overlay",
         "shelly": "shelly",
         "treefmt-nix": "treefmt-nix"
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -17,6 +17,9 @@
       url = "github:numtide/treefmt-nix";
       inputs.nixpkgs.follows = "nixpkgs";
     };
+    # DeterminateSystem's nix fork is controversial, but I don't mind it,
+    # and it has lazy-trees support which is useful for fleet.
+    nix.url = "/home/lach/build/nix-src";
   };
   outputs =
     inputs:
@@ -41,6 +44,11 @@
 
           fleetModules.tf = ./modules/extras/tf.nix;
 
+          testObj = {
+            v = "Hello";
+          };
+          testString = "hello";
+
           # To be used with https://github.com/NixOS/nix/pull/8892
           schemas =
             let
@@ -80,6 +88,7 @@
             system,
             pkgs,
             self,
+            inputs',
             ...
           }:
           let
@@ -108,7 +117,7 @@
             packages = lib.mkIf deployerSystem (
               let
                 packages = pkgs.callPackages ./pkgs {
-                  inherit craneLib;
+                  inherit craneLib inputs';
                 };
               in
               packages // { default = packages.fleet; }
@@ -120,6 +129,7 @@
                 nixpkgsCraneLib = inputs.crane.mkLib pkgs;
                 packages = pkgs.callPackages ./pkgs {
                   craneLib = nixpkgsCraneLib;
+                  inherit inputs;
                 };
                 prefixAttrs =
                   prefix: attrs:
@@ -150,13 +160,16 @@
                 cargo-fuzz
                 cargo-watch
                 cargo-outdated
+                gdb
 
                 pkg-config
                 openssl
                 bacon
                 nil
                 rustPlatform.bindgenHook
-                # nixVersions.nix_2_22
+                inputs'.nix.packages.nix-expr-c
+                inputs'.nix.packages.nix-flake-c
+                inputs'.nix.packages.nix-fetchers-c
               ];
               environment.PROTOC = "${pkgs.protobuf}/bin/protoc";
             };
modifiedlib/flakePart.nixdiffbeforeafterboth
--- a/lib/flakePart.nix
+++ b/lib/flakePart.nix
@@ -65,7 +65,7 @@
           normalEval = bootstrapNixpkgs.lib.evalModules {
             modules = (import ../modules/module-list.nix) ++ [
               module
-              {
+              ({inputs', ...}: {
                 config = {
                   data = if isPath data then import data else data;
                   nixpkgs.buildUsing = mkOptionDefault bootstrapNixpkgs;
@@ -74,6 +74,7 @@
                       inherit
                         (import ../pkgs {
                           inherit (prev) callPackage;
+                          inherit inputs';
                           craneLib = crane.mkLib prev;
                         })
                         fleet-install-secrets
@@ -82,7 +83,7 @@
                     })
                   ];
                 };
-              }
+              })
             ];
             specialArgs = {
               inherit inputs self;
modifiedmodules/nixos/online.nixdiffbeforeafterboth
--- a/modules/nixos/online.nix
+++ b/modules/nixos/online.nix
@@ -80,7 +80,8 @@
       '';
       supportsDryActivation = true;
     };
-  } // config.system.onlineActivationScripts;
+  }
+  // config.system.onlineActivationScripts;
 
   config.systemd.services = mkIf config.networking.networkmanager.enable {
     # If machine is managed by fleet, we should not restart NetworkManager during activation,
modifiedmodules/secrets-data.nixdiffbeforeafterboth
--- a/modules/secrets-data.nix
+++ b/modules/secrets-data.nix
@@ -105,7 +105,7 @@
         description = "Age-compatible key";
       };
     };
-    config = {};
+    config = { };
   };
 in
 {
modifiedpkgs/default.nixdiffbeforeafterboth
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,9 +1,10 @@
 {
   callPackage,
   craneLib,
+  inputs',
 }:
 {
-  fleet = callPackage ./fleet.nix { inherit craneLib; };
+  fleet = callPackage ./fleet.nix { inherit craneLib inputs'; };
   fleet-install-secrets = callPackage ./fleet-install-secrets.nix { inherit craneLib; };
   fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; };
 }
modifiedpkgs/fleet.nixdiffbeforeafterboth
--- a/pkgs/fleet.nix
+++ b/pkgs/fleet.nix
@@ -1,16 +1,35 @@
 {
+  lib,
   craneLib,
   installShellFiles,
+  inputs',
+  pkg-config,
+  rustPlatform,
 }:
 craneLib.buildPackage rec {
   pname = "fleet";
-
-  src = craneLib.cleanCargoSource (craneLib.path ../.);
+  src = lib.cleanSourceWith {
+    src = ../.;
+    filter =
+      path: type:
+      (lib.hasSuffix "\.cc" path)
+      || (lib.hasSuffix "\.hh" path)
+      || (craneLib.filterCargoSources path type);
+  };
   strictDeps = true;
 
   cargoExtraArgs = "--locked -p ${pname}";
 
-  nativeBuildInputs = [ installShellFiles ];
+  buildInputs = [
+    inputs'.nix.packages.nix-expr-c
+    inputs'.nix.packages.nix-flake-c
+    inputs'.nix.packages.nix-fetchers-c
+  ];
+  nativeBuildInputs = [
+    installShellFiles
+    pkg-config
+    rustPlatform.bindgenHook
+  ];
 
   postInstall = ''
     for shell in bash fish zsh; do
modifiedtreefmt.nixdiffbeforeafterboth
--- a/treefmt.nix
+++ b/treefmt.nix
@@ -9,4 +9,5 @@
   programs.shfmt.enable = true;
   programs.rustfmt.enable = true;
   programs.taplo.enable = true;
+  programs.clang-format.enable = true;
 }