git.delta.rocks / jrsonnet / refs/commits / 69498f520d8e

difftreelog

feat on-demand secret generation

rxzprzppYaroslav Bolyukin2026-01-22parent: #faec707.patch.diff
in: trunk

24 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1149,7 +1149,6 @@
  "base64 0.22.1",
  "serde",
  "unicode_categories",
- "z85",
 ]
 
 [[package]]
@@ -2089,6 +2088,7 @@
  "serde_json",
  "test-log",
  "thiserror 2.0.17",
+ "tokio",
  "tracing",
  "tracing-indicatif",
  "vte 0.15.0",
@@ -4638,12 +4638,6 @@
  "syn",
  "synstructure",
 ]
-
-[[package]]
-name = "z85"
-version = "3.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64"
 
 [[package]]
 name = "zerocopy"
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -7,8 +7,9 @@
 	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
+use futures::{StreamExt as _, stream::FuturesUnordered};
 use nix_eval::nix_go;
-use tokio::task::{LocalSet, spawn_blocking};
+use tokio::task::spawn_blocking;
 use tracing::{Instrument, error, field, info, info_span, warn};
 
 #[derive(Parser)]
@@ -47,7 +48,7 @@
 				"--profile",
 				format!(
 					"/nix/var/nix/profiles/{}-{hostname}",
-					config.data().gc_root_prefix
+					config.data.gc_root_prefix
 				),
 			)
 			.arg(&out_output);
@@ -60,14 +61,14 @@
 impl BuildSystems {
 	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
 		let hosts = opts.filter_skipped(config.list_hosts()?)?;
-		let set = LocalSet::new();
+		let mut tasks = FuturesUnordered::new();
 		let build_attr = self.build_attr.clone();
 		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();
-			set.spawn_local(
+			tasks.push(
 				(async move {
 					let built = match build_task(config, hostname.clone(), &build_attr).await {
 						Ok(path) => path,
@@ -88,7 +89,7 @@
 				.instrument(span),
 			);
 		}
-		set.await;
+		for _task in tasks.next().await {}
 		Ok(())
 	}
 }
@@ -96,7 +97,7 @@
 impl Deploy {
 	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
 		let hosts = opts.filter_skipped(config.list_hosts()?)?;
-		let set = LocalSet::new();
+		let mut tasks = FuturesUnordered::new();
 		for host in hosts.into_iter() {
 			let config = config.clone();
 			let span = info_span!("deploy", host = field::display(&host.name));
@@ -112,7 +113,7 @@
 				host.set_legacy_ssh_store(legacy);
 			};
 
-			set.spawn_local(
+			tasks.push(
 				(async move {
 					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")
 						.await
@@ -170,7 +171,7 @@
 				.instrument(span),
 			);
 		}
-		set.await;
+		for _task in tasks.next().await {}
 		Ok(())
 	}
 }
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -56,7 +56,7 @@
 		.collect::<HashSet<_>>();
 	let mut stored_locally = config
 		.local_host()
-		.list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))
+		.list_generations(&format!("{}-{}", config.data.gc_root_prefix, host.name))
 		.await
 		.inspect_err(|e| {
 			warn!("failed to list generations available locally: {e}");
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -412,7 +412,7 @@
 					if opts.should_skip(&host)? {
 						continue;
 					}
-					config.key(&host.name).await?;
+					config.host_key(&host.name).await?;
 				}
 			}
 			Secret::Read {
@@ -421,6 +421,7 @@
 				part: part_name,
 				mut prefer_identities,
 			} => {
+				/*
 				let Some(secret) = config.shared_secret(&name) else {
 					bail!("secret doesn't exists");
 				};
@@ -460,6 +461,8 @@
 					part.raw.data.clone()
 				};
 				stdout().write_all(&data)?;
+				*/
+				todo!()
 			}
 			Secret::Regenerate {
 				prefer_identities,
@@ -605,6 +608,7 @@
 				todo!()
 			}
 			Secret::List {} => {
+				/*
 				let _span = info_span!("loading secrets").entered();
 				let configured = config.list_configured_shared()?;
 				#[derive(Tabled)]
@@ -638,6 +642,8 @@
 					*/
 				}
 				// info!("loaded\n{}", Table::new(table).to_string())
+				*/
+				todo!()
 			}
 			Secret::Edit {
 				name,
@@ -645,7 +651,7 @@
 				part,
 				add,
 			} => {
-				let secret = config
+				/*let secret = config
 					.host_secret(&machine, &name)
 					.context("secret not found")?;
 				if let Some(data) = secret.secret.parts.get(&part) {
@@ -656,7 +662,8 @@
 					String::new()
 				} else {
 					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");
-				};
+				};*/
+				todo!()
 			}
 		}
 		Ok(())
modifiedcmds/fleet/src/cmds/tf.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/tf.rs
+++ b/cmds/fleet/src/cmds/tf.rs
@@ -72,9 +72,9 @@
 			let tf_data: TfData = serde_json::from_slice(&data.stdout)
 				.context("failed to parse terraform fleet output")?;
 
-			let mut data = config.data();
 			debug!("synchronized done = {tf_data:?}");
-			data.extra.insert(
+			let mut extra = config.data.extra.write().expect("no poisoning");
+			extra.insert(
 				"terraformHosts".to_owned(),
 				serde_json::to_value(tf_data.hosts).expect("should be valid extra"),
 			);
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -4,7 +4,7 @@
 // pub(crate) mod command;
 pub(crate) mod extra_args;
 
-use std::{env, ffi::OsString, process::ExitCode};
+use std::{env, ffi::OsString, process::ExitCode, sync::Arc};
 
 use anyhow::{Result, bail};
 use clap::{CommandFactory, Parser};
@@ -23,7 +23,9 @@
 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 nix_eval::{
+	gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,
+};
 use tracing::{Instrument, error, info, info_span};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::IndicatifLayer;
@@ -39,9 +41,9 @@
 			info!("nothing to prefetch: no prefetch directory");
 			return Ok(());
 		}
-		let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();
+		let tasks = FuturesUnordered::new();
 		for entry in std::fs::read_dir(&prefetch_dir)? {
-			tasks.push(Box::pin(async {
+			tasks.push(async {
 				let entry = entry?;
 				if !entry.metadata()?.is_file() {
 					bail!("only files should exist in prefetch directory");
@@ -59,7 +61,7 @@
 				status.arg("store").arg("prefetch-file").arg(path);
 				status.run_nix_string().instrument(span).await?;
 				Ok(())
-			}));
+			});
 		}
 		tasks.try_collect::<Vec<()>>().await?;
 		Ok(())
@@ -190,7 +192,7 @@
 
 	init_libraries();
 
-	tokio::runtime::Builder::new_multi_thread()
+	let runtime = tokio::runtime::Builder::new_multi_thread()
 		.enable_all()
 		.on_thread_start(|| {
 			gc_register_my_thread();
@@ -199,8 +201,13 @@
 			gc_unregister_my_thread();
 		})
 		.build()
-		.expect("failed to build runtime")
-		.block_on(async {
+		.expect("failed to build runtime");
+	let runtime = Arc::new(runtime);
+
+	init_tokio_for_nix(runtime.clone());
+
+	runtime.block_on(async {
+		tokio::task::spawn(async move {
 			if let Err(e) = main_real(opts).await {
 				error!("{e:#}");
 				ExitCode::FAILURE
@@ -208,6 +215,9 @@
 				ExitCode::SUCCESS
 			}
 		})
+		.await
+		.expect("primary task panicked")
+	})
 	// async_main(opts)
 }
 
modifiedcrates/fleet-base/src/command.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/command.rs
+++ b/crates/fleet-base/src/command.rs
@@ -334,7 +334,7 @@
 	let mut stderr = child.stderr.take().unwrap();
 	let stdout = child.stdout.take().unwrap();
 	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
-	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));
+	let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
 	let mut ob = want_stdout
 		.then(|| out.take().unwrap())
 		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
@@ -397,7 +397,7 @@
 	let mut stderr = child.stderr().take().unwrap();
 	let stdout = child.stdout().take().unwrap();
 	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
-	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));
+	let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
 	let mut ob = want_stdout
 		.then(|| out.take().unwrap())
 		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -78,7 +78,7 @@
 	// unit name conflict in systemd-run
 	// This code is tied to rollback.nix
 	if !disable_rollback && action.should_create_rollback_marker() {
-		let _span = info_span!("preparing").entered();
+		// let _span = info_span!("preparing").entered();
 		info!("preparing for rollback");
 		let generation = get_current_generation(host).await?;
 		info!(
@@ -179,7 +179,7 @@
 		// FIXME: Connection might be disconnected after activation run
 
 		if action.should_activate() && !failed {
-			let _span = info_span!("activating").entered();
+			// let _span = info_span!("activating").entered();
 			info!("executing activation script");
 			let specialised = if let Some(specialisation) = specialisation {
 				let mut specialised = built.join("specialisation");
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -1,10 +1,12 @@
 use std::{
+	cmp::Ordering,
 	collections::{
 		BTreeMap, BTreeSet,
 		btree_map::{self, Entry},
 	},
+	fmt,
 	io::{self, Cursor},
-	ops::Deref,
+	sync::RwLock,
 };
 
 use age::Recipient;
@@ -77,19 +79,18 @@
 	pub manager_keys: Vec<ManagerKey>,
 
 	#[serde(default)]
-	pub hosts: BTreeMap<String, HostData>,
+	pub hosts: RwLock<BTreeMap<String, HostData>>,
 
 	#[serde(default, alias = "shared_secrets")]
-	pub secrets: FleetSecrets,
+	pub secrets: RwLock<FleetSecrets>,
 
 	// extra_name => anything
 	#[serde(default)]
-	#[serde(skip_serializing_if = "BTreeMap::is_empty")]
-	pub extra: BTreeMap<String, Value>,
+	pub extra: RwLock<BTreeMap<String, Value>>,
 
 	#[serde(default)]
-	#[serde(skip_serializing_if = "BTreeMap::is_empty")]
-	host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
+	#[serde(skip_serializing)]
+	host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
 }
 impl FleetData {
 	pub fn from_str(s: &str) -> anyhow::Result<Self> {
@@ -97,6 +98,8 @@
 		if !data.host_secrets.is_empty() {
 			info!("migrating host secrets into shared secrets structure");
 			data.secrets
+				.write()
+				.expect("no poisoning")
 				.merge_from_hosts(std::mem::take(&mut data.host_secrets));
 		}
 		Ok(data)
@@ -130,128 +133,431 @@
 #[serde(rename_all = "camelCase")]
 #[must_use]
 pub struct FleetSecretData {
-	#[serde(default = "Utc::now")]
 	pub created_at: DateTime<Utc>,
-	#[serde(default)]
-	#[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]
+	#[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]
 	pub expires_at: Option<DateTime<Utc>>,
 
 	#[serde(flatten)]
 	pub parts: BTreeMap<String, FleetSecretPart>,
 
-	#[serde(default)]
-	#[serde(skip_serializing_if = "Value::is_null")]
+	#[serde(default, skip_serializing_if = "Value::is_null")]
 	pub generation_data: Value,
 }
 
+fn is_false(b: &bool) -> bool {
+	*b == false
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct SecretOwner(String);
+
+impl fmt::Display for SecretOwner {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "host:{}", self.0)
+	}
+}
+
+impl SecretOwner {
+	pub fn host(s: impl AsRef<str>) -> SecretOwner {
+		SecretOwner(s.as_ref().to_owned())
+	}
+	pub fn as_host(&self) -> Option<&str> {
+		Some(&self.0)
+	}
+}
+
 #[derive(Serialize, Deserialize, Clone, Debug)]
 #[serde(rename_all = "camelCase")]
 #[must_use]
 pub struct FleetSecretDistribution {
 	#[serde(default)]
-	pub owners: BTreeSet<String>,
+	owners: BTreeSet<SecretOwner>,
+	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+	owners_pending_prune: BTreeMap<SecretOwner, String>,
+
 	#[serde(flatten)]
 	pub secret: FleetSecretData,
 
+	#[serde(default, skip_serializing_if = "Option::is_none")]
+	pending_prune: Option<String>,
 	#[serde(default, skip_serializing, alias = "managed")]
-	pub _deprecated_managed: bool,
+	_deprecated_managed: bool,
+}
+
+const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();
+impl FleetSecretDistribution {
+	pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {
+		assert!(
+			!owners.is_empty(),
+			"distribution should have at least one owner"
+		);
+		if let Some(expires_at) = &secret.expires_at {
+			assert!(
+				*expires_at > now,
+				"secret should not be expired on creation"
+			);
+		}
+		Self {
+			owners,
+			secret,
+			owners_pending_prune: BTreeMap::new(),
+			pending_prune: None,
+			_deprecated_managed: true,
+		}
+	}
+
+	fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {
+		let pending_prune = if including_pruned {
+			&self.owners_pending_prune
+		} else {
+			EMPTY_PENDING_PRUNE
+		};
+		self.owners.iter().chain(pending_prune.keys())
+	}
+	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
+		self.owners_ex(false)
+	}
+
+	pub fn prune(&mut self, reason: String) {
+		assert!(
+			self.pending_prune.is_none(),
+			"it shouldn't be possible to prune the same distribution twice using public api"
+		);
+		self.pending_prune = Some(reason);
+	}
+	pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {
+		// if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {
+		// 	self.prune(format!("all owners were pruned: {reason}"));
+		// 	return;
+		// }
+		for owner in owners {
+			if self.owners.remove(owner) {
+				self.owners_pending_prune
+					.insert(owner.to_owned(), reason.clone());
+			}
+		}
+		// if self.owners.is_empty() {
+		// 	self.prune("no owners left".to_owned());
+		// }
+	}
+	pub fn unprune_owner(&mut self, owner: SecretOwner) {
+		if self.owners_pending_prune.remove(&owner).is_some() {
+			self.owners.insert(owner);
+		}
+	}
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, Default)]
 #[must_use]
-pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);
+pub struct FleetSecretDistributions {
+	stored: Vec<FleetSecretDistribution>,
+}
 
-impl Deref for FleetSecretDistributions {
-	type Target = [FleetSecretDistribution];
-
-	fn deref(&self) -> &Self::Target {
-		self.0.as_slice()
+fn compare_dists(
+	a: &FleetSecretDistribution,
+	b: &FleetSecretDistribution,
+	prefer_identities: &BTreeSet<SecretOwner>,
+	include_pruned_owners: bool,
+) -> Ordering {
+	use Ordering::*;
+	if prefer_identities.is_empty() {
+		let a_has = a
+			.owners_ex(include_pruned_owners)
+			.any(|o| prefer_identities.contains(o));
+		let b_has = b
+			.owners_ex(include_pruned_owners)
+			.any(|o| prefer_identities.contains(o));
+		match (a_has, b_has) {
+			(true, false) => return Greater,
+			(false, true) => return Less,
+			_ => {}
+		}
+	}
+	match (a.secret.expires_at, b.secret.expires_at) {
+		(None, Some(_)) => return Greater,
+		(Some(_), None) => return Less,
+		(Some(a), Some(b)) => {
+			// Later is better
+			return a.cmp(&b);
+		}
+		(None, None) => {}
 	}
+
+	// Which one is easier to access
+	return a.owners.len().cmp(&b.owners.len());
 }
 
 impl FleetSecretDistributions {
-	pub fn owners(&self) -> impl Iterator<Item = &String> {
-		self.0.iter().flat_map(|v| v.owners.iter())
+	/// Drop expired distributions
+	fn prune_expired(&mut self, now: DateTime<Utc>) {
+		for ele in self.distributions_mut() {
+			if let Some(expires_at) = ele.secret.expires_at {
+				if expires_at < now {
+					ele.prune(format!("expired during check at {now}"));
+				}
+			}
+		}
+	}
+	/// Perform all pruning relevant to shared secrets
+	/// Also see expected_owner_removed
+	pub fn prune_shared(
+		&mut self,
+		expected_owners: &BTreeSet<SecretOwner>,
+		unique: bool,
+		expected_parts: &BTreeMap<String, GeneratorPart>,
+		expected_generation_data: &Value,
+		regenerate_on_owner_removed: bool,
+		regenerate_on_owner_added: bool,
+		prefer_identities: &BTreeSet<SecretOwner>,
+		now: DateTime<Utc>,
+	) {
+		self.prune_expired(now);
+		self.prune_generation_data(expected_generation_data, None);
+		self.prune_missing_parts(expected_parts, None);
+
+		let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();
+
+		let mut to_add = expected_owners.difference(&current_owners);
+		if to_add.next().is_some() && unique && regenerate_on_owner_added {
+			for dist in self.distributions_mut() {
+				dist.prune(format!(
+					"owners missing, can't add new distribution, regeneration preferred"
+				));
+			}
+			return;
+		}
+
+		for to_remove in current_owners.difference(&expected_owners) {
+			self.entry(to_remove.clone()).remove(
+				regenerate_on_owner_removed,
+				"owner was removed from expected owners list, regenerate_on_owner_removed is set"
+					.to_string(),
+			);
+		}
+		if unique {
+			self.prune_nonunique(prefer_identities);
+		}
+	}
+	pub fn prune_host(
+		&mut self,
+		owner: SecretOwner,
+		expected_parts: &BTreeMap<String, GeneratorPart>,
+		expected_generation_data: &Value,
+		now: DateTime<Utc>,
+	) {
+		self.prune_expired(now);
+		self.prune_generation_data(expected_generation_data, Some(&owner));
+		// TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)
+		self.prune_missing_parts(expected_parts, Some(&owner));
+	}
+	/// Position of best distributions as in iterator returned by distributions()
+	/// None if distributions not found
+	fn best_idx(
+		&self,
+		prefer_identities: &BTreeSet<SecretOwner>,
+		include_pruned_owners: bool,
+	) -> Option<usize> {
+		self.distributions()
+			.enumerate()
+			.max_by(|(_, a), (_, b)| {
+				compare_dists(&a, &b, prefer_identities, include_pruned_owners)
+			})
+			.map(|(p, _)| p)
+	}
+	/// Secret wants to be the same on all hosts, leave only one unpruned version of it
+	fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {
+		if self.distributions().next().is_none() {
+			return;
+		}
+		let best = self.best_idx(prefer_identities, false).expect("not empty");
+		for (i, dist) in self.distributions_mut().enumerate() {
+			if i != best {
+				dist.prune(
+					"secret wants to be the same on all hosts, only the best one was left"
+						.to_owned(),
+				);
+			}
+		}
+	}
+
+	pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {
+		assert!(self.get(&owner).is_none(), "secret is not pruned for host");
+		if let Some(dist) = self
+			.distributions_mut()
+			.find(|v| v.owners_pending_prune.contains_key(&owner))
+		{
+			dist.unprune_owner(owner);
+			Some(dist)
+		} else {
+			None
+		}
+	}
+
+	pub fn best_distribution_for_reencryption(
+		&mut self,
+		prefer_identities: &BTreeSet<SecretOwner>,
+	) -> Option<&mut FleetSecretDistribution> {
+		let best_idx = self.best_idx(prefer_identities, true)?;
+		self.distributions_mut().nth(best_idx)
+	}
+
+	fn prune_missing_parts(
+		&mut self,
+		expected_parts: &BTreeMap<String, GeneratorPart>,
+		filter_owner: Option<&SecretOwner>,
+	) {
+		'dist: for ele in self.distributions_mut() {
+			if let Some(filter_owner) = filter_owner {
+				if !ele.owners.contains(filter_owner) {
+					continue;
+				}
+				// Note: secret still can have multiple owners even if it is host-owned
+				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them
+			}
+			for (name, part) in expected_parts {
+				let Some(stored_part) = ele.secret.parts.get(name) else {
+					ele.prune(format!("secret definition added new part: {name}"));
+					continue 'dist;
+				};
+				if part.encrypted != stored_part.raw.encrypted {
+					ele.prune(format!(
+						"secret definition now requires part to be {}",
+						if part.encrypted {
+							"encrypted"
+						} else {
+							"non-encrypted"
+						}
+					));
+					continue 'dist;
+				}
+			}
+		}
+	}
+	fn prune_generation_data(
+		&mut self,
+		expected_generation_data: &Value,
+		filter_owner: Option<&SecretOwner>,
+	) {
+		for ele in self.distributions_mut() {
+			if let Some(filter_owner) = filter_owner {
+				if !ele.owners.contains(filter_owner) {
+					continue;
+				}
+				// Note: secret still can have multiple owners even if it is host-owned
+				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them
+			}
+			if ele.secret.generation_data != *expected_generation_data {
+				ele.prune(format!(
+					"expected generation data mismatch: {expected_generation_data:?}"
+				));
+			}
+		}
 	}
+
+	/// Prune all distributions with no unpruned owners.
+	/// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and
+	/// can decrypt their secrets.
+	fn prune_dead(&mut self) {
+		for ele in self.distributions_mut() {
+			if ele.owners.is_empty() {
+				ele.prune("no owners left".to_owned());
+			}
+		}
+	}
+
+	pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {
+		self.stored.iter().filter(|v| v.pending_prune.is_none())
+	}
+	pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {
+		self.stored.iter_mut().filter(|v| v.pending_prune.is_none())
+	}
+	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
+		self.distributions().flat_map(|v| v.owners.iter())
+	}
 	#[allow(
 		clippy::len_without_is_empty,
 		reason = "should not be empty for a long time"
 	)]
 	pub fn len(&self) -> usize {
-		self.0.len()
+		self.distributions().count()
 	}
 
-	pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {
-		self.0.iter().find(|d| d.owners.contains(owner))
+	pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {
+		self.distributions().find(|d| d.owners.contains(owner))
 	}
-	fn entry(&mut self, owner: String) -> DistEntry<'_> {
-		let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {
+	fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {
+		let Some((idx, dist)) = self
+			.distributions()
+			.enumerate()
+			.find(|(_, d)| d.owners.contains(&owner))
+		else {
 			return DistEntry::Vacant(VacantDistEntry {
 				distributions: self,
-				owner,
+				owners: BTreeSet::from([owner]),
 			});
 		};
 		DistEntry::Occupied(OccupiedDistEntry {
+			owners: dist.owners.clone(),
 			distributions: self,
 			idx,
-			owner,
 		})
 	}
-	fn extend(&mut self, dist: FleetSecretDistribution) {
-		for owner in &dist.owners {
-			self.entry(owner.to_owned()).remove();
+	pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {
+		for ele in self.distributions_mut() {
+			ele.prune_owners(&dist.owners, reason.clone());
 		}
-		self.0.push(dist);
+		self.stored.push(dist);
 	}
-	pub fn contains(&self, owner: &str) -> bool {
-		self.0.iter().any(|d| d.owners.contains(owner))
+	pub fn contains(&self, owner: &SecretOwner) -> bool {
+		self.distributions().any(|d| d.owners.contains(owner))
 	}
 }
 
 struct OccupiedDistEntry<'d> {
 	distributions: &'d mut FleetSecretDistributions,
 	idx: usize,
-	owner: String,
+	owners: BTreeSet<SecretOwner>,
 }
 impl<'d> OccupiedDistEntry<'d> {
-	fn remove(self) -> VacantDistEntry<'d> {
-		let dist = &mut self.distributions.0[self.idx];
-		assert!(
-			dist.owners.remove(&self.owner),
-			"entry exists, as we have its reference"
-		);
-		if dist.owners.is_empty() {
-			self.distributions.0.remove(self.idx);
+	fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {
+		let dist = &mut self.distributions.stored[self.idx];
+		if whole_dist {
+			dist.prune(reason);
+		} else {
+			dist.prune_owners(&self.owners, reason);
 		}
 		VacantDistEntry {
 			distributions: self.distributions,
-			owner: self.owner,
+			owners: self.owners,
 		}
 	}
-	fn set(self, secret: FleetSecretData) -> Self {
-		self.remove().set(secret)
+	fn set(self, secret: FleetSecretData, reason: String) -> Self {
+		self.remove(false, reason).set(secret)
 	}
 }
 struct VacantDistEntry<'d> {
 	distributions: &'d mut FleetSecretDistributions,
-	owner: String,
+	owners: BTreeSet<SecretOwner>,
 }
 impl<'d> VacantDistEntry<'d> {
 	fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {
 		let Self {
 			distributions,
-			owner,
+			owners,
 		} = self;
-		let idx = distributions.0.len();
-		distributions.0.push(FleetSecretDistribution {
-			owners: BTreeSet::from_iter([owner.clone()]),
+		let idx = distributions.stored.len();
+		distributions.stored.push(FleetSecretDistribution {
+			owners: owners.clone(),
 			secret,
 
+			owners_pending_prune: BTreeMap::new(),
+			pending_prune: None,
 			_deprecated_managed: true,
 		});
 		OccupiedDistEntry {
 			distributions,
-			owner,
+			owners,
 			idx,
 		}
 	}
@@ -262,16 +568,16 @@
 	Occupied(OccupiedDistEntry<'d>),
 }
 impl DistEntry<'_> {
-	fn remove(self) -> Self {
+	fn remove(self, whole_dist: bool, reason: String) -> Self {
 		match self {
 			DistEntry::Vacant(_) => self,
-			DistEntry::Occupied(o) => Self::Vacant(o.remove()),
+			DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),
 		}
 	}
-	fn set(self, secret: FleetSecretData) -> Self {
+	fn set(self, secret: FleetSecretData, reason: String) -> Self {
 		Self::Occupied(match self {
 			DistEntry::Vacant(e) => e.set(secret),
-			DistEntry::Occupied(e) => e.set(secret),
+			DistEntry::Occupied(e) => e.set(secret, reason),
 		})
 	}
 }
@@ -281,8 +587,13 @@
 	where
 		S: serde::Serializer,
 	{
+		let mut v = self.clone();
+		v.prune_dead();
 		let mut found_hosts = BTreeSet::new();
-		for ele in self.0.iter() {
+		for ele in v.distributions() {
+			if ele.pending_prune.is_some() {
+				continue;
+			}
 			if ele.owners.is_empty() {
 				panic!("consistency: secret distribution has no defined owners");
 			}
@@ -294,10 +605,15 @@
 				}
 			}
 		}
-		match self.0.len() {
+		match v.stored.len() {
 			0 => panic!("consistency: empty distributions"),
-			1 => self.0[0].serialize(serializer),
-			_ => self.0.serialize(serializer),
+			1 => v.stored[0].serialize(serializer),
+			_ => {
+				let mut sorted = v.stored.clone();
+				// Store outdated distributions last
+				sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);
+				sorted.serialize(serializer)
+			}
 		}
 	}
 }
@@ -313,15 +629,18 @@
 			Many(Vec<FleetSecretDistribution>),
 		}
 		let d = Distributions::deserialize(deserializer)?;
-		let ds = match d {
+		let stored = match d {
 			Distributions::One(d) => vec![d],
 			Distributions::Many(ds) => ds,
 		};
-		if ds.is_empty() {
+		if stored.is_empty() {
 			return Err(de::Error::custom("consistency: empty distributions"));
 		}
 		let mut found_hosts = BTreeSet::new();
-		for ele in ds.iter() {
+		for ele in stored.iter() {
+			if ele.pending_prune.is_some() {
+				continue;
+			}
 			if ele.owners.is_empty() {
 				return Err(de::Error::custom(
 					"consistency: secret distribution has no defined owners",
@@ -335,73 +654,65 @@
 				}
 			}
 		}
-		Ok(Self(ds))
+		Ok(Self { stored })
 	}
 }
 
-#[derive(Serialize, Deserialize, Default)]
+#[derive(Deserialize, Default)]
 pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);
 
+impl Serialize for FleetSecrets {
+	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+	where
+		S: serde::Serializer,
+	{
+		let data: BTreeMap<String, FleetSecretDistributions> = self
+			.0
+			.iter()
+			.filter(|(_, v)| !v.stored.is_empty())
+			.map(|(k, v)| (k.clone(), v.clone()))
+			.collect();
+
+		data.serialize(serializer)
+	}
+}
+
 impl FleetSecrets {
 	pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {
 		self.0.keys()
 	}
 
-	pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {
+	pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {
 		self.0
 			.iter()
 			.filter(|(_, d)| d.contains(owner))
 			.map(|(n, _)| n)
 	}
 
-	pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {
-		let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {
-			return false;
-		};
-		let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {
-			return false;
-		};
-
-		dist.remove();
-
-		if dists.get().0.is_empty() {
-			dists.remove();
-		};
-
-		true
-	}
-	pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {
-		let e = self
-			.0
-			.entry(secret.to_owned())
-			.or_insert_with(|| FleetSecretDistributions(Default::default()));
-		e.entry(owner.to_owned()).set(data);
-	}
 	pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {
 		match self.0.entry(secret) {
 			Entry::Vacant(e) => {
-				e.insert(FleetSecretDistributions(vec![data]));
+				e.insert(FleetSecretDistributions { stored: vec![data] });
 			}
 			Entry::Occupied(mut e) => {
 				let dists = e.get_mut();
-				dists.extend(data)
+				dists.extend(data, "secret data was replaced".to_owned())
 			}
 		}
-	}
-	pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {
-		let secret = self.0.get(secret)?;
-		secret.get(owner)
 	}
 	pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {
 		self.0.get(secret)
 	}
+	pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {
+		self.0.get_mut(secret)
+	}
 
-	pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {
-		let Some(secret) = self.0.get(secret) else {
-			return false;
-		};
-		secret.contains(owner)
+	pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {
+		self.0
+			.entry(secret.to_owned())
+			.or_insert(FleetSecretDistributions::default())
 	}
+
 	pub fn contains(&self, secret: &str) -> bool {
 		self.0.contains_key(secret)
 	}
@@ -411,7 +722,7 @@
 
 	fn merge_from_hosts(
 		&mut self,
-		host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
+		host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
 	) {
 		for (host, host_secrets) in host_secrets {
 			for (secret_name, mut secret_data) in host_secrets {
@@ -420,11 +731,27 @@
 			}
 		}
 	}
+
+	pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {
+		for (name, dists) in self.0.iter_mut() {
+			if expected_nonshared.contains(name) {
+				continue;
+			}
+			for dist in dists.distributions_mut() {
+				if dist.owners.contains(host) {
+					dist.prune_owners(
+						&BTreeSet::from([host.to_owned()]),
+						"host no longer defines this secret".to_owned(),
+					);
+				}
+			}
+		}
+	}
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct Expectations {
-	pub owners: BTreeSet<String>,
+	pub owners: BTreeSet<SecretOwner>,
 	pub generation_data: serde_json::Value,
 	pub parts: BTreeMap<String, GeneratorPart>,
 }
@@ -432,3 +759,26 @@
 pub struct GeneratorPart {
 	pub encrypted: bool,
 }
+
+#[derive(Debug, Clone, Copy)]
+pub struct RegenerationConstraints {
+	pub allow_different: bool,
+	pub regenerate_on_owner_added: bool,
+	pub regenerate_on_owner_removed: bool,
+}
+impl RegenerationConstraints {
+	pub fn host_personal() -> Self {
+		Self {
+			allow_different: false,
+			regenerate_on_owner_added: true,
+			regenerate_on_owner_removed: true,
+		}
+	}
+	pub fn without_preferences(self) -> Self {
+		Self {
+			allow_different: self.allow_different,
+			regenerate_on_owner_added: false,
+			regenerate_on_owner_removed: false,
+		}
+	}
+}
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -1,6 +1,5 @@
 use std::{
-	cell::OnceCell,
-	collections::BTreeSet,
+	collections::{BTreeMap, BTreeSet},
 	ffi::{OsStr, OsString},
 	fmt::Display,
 	io::Write,
@@ -11,6 +10,7 @@
 };
 
 use anyhow::{Context, Result, anyhow, bail, ensure};
+use chrono::{DateTime, Utc};
 use fleet_shared::SecretData;
 use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
 use openssh::{ControlPersist, SessionBuilder};
@@ -22,15 +22,20 @@
 
 use crate::{
 	command::MyCommand,
-	fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},
+	fleetdata::{
+		FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,
+	},
 };
 
 pub struct FleetConfigInternals {
+	pub prefer_identities: BTreeSet<SecretOwner>,
+	pub now: DateTime<Utc>,
+
 	/// Fleet project directory, containing fleet.nix file.
 	pub directory: PathBuf,
 	/// builtins.currentSystem
 	pub local_system: String,
-	pub data: Arc<Mutex<FleetData>>,
+	pub data: Arc<FleetData>,
 	pub nix_args: Vec<OsString>,
 	/// fleet_config.config
 	pub config_field: Value,
@@ -96,16 +101,16 @@
 pub struct ConfigHost {
 	config: Config,
 	pub name: String,
-	groups: OnceCell<Vec<String>>,
+	groups: OnceLock<Vec<String>>,
 
 	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
-	deploy_kind: OnceCell<DeployKind>,
-	session_destination: OnceCell<String>,
-	legacy_ssh_store: OnceCell<bool>,
+	deploy_kind: OnceLock<DeployKind>,
+	session_destination: OnceLock<String>,
+	legacy_ssh_store: OnceLock<bool>,
 
 	pub host_config: Option<Value>,
-	pub nixos_config: OnceCell<Value>,
-	pub nixos_unchecked_config: OnceCell<Value>,
+	pub nixos_config: OnceLock<Value>,
+	pub nixos_unchecked_config: OnceLock<Value>,
 	pub pkgs_override: Option<Value>,
 
 	// TODO: Move command helpers away with connectivity refactor
@@ -397,7 +402,38 @@
 		ensure!(!data.encrypted, "secret came out encrypted");
 		Ok(data.data)
 	}
-	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {
+	pub async fn reencrypt_distribution(
+		&self,
+		data: &FleetSecretDistribution,
+		targets: BTreeSet<SecretOwner>,
+		now: DateTime<Utc>,
+	) -> Result<FleetSecretDistribution> {
+		let mut parts = BTreeMap::new();
+		for (part_name, part) in &data.secret.parts {
+			parts.insert(
+				part_name.clone(),
+				if part.raw.encrypted {
+					FleetSecretPart {
+						raw: self.reencrypt(part.raw.clone(), targets.clone()).await?,
+					}
+				} else {
+					part.clone()
+				},
+			);
+		}
+		let secret = FleetSecretData {
+			created_at: data.secret.created_at,
+			expires_at: data.secret.expires_at,
+			generation_data: data.secret.generation_data.clone(),
+			parts,
+		};
+		Ok(FleetSecretDistribution::new(targets, secret, now))
+	}
+	pub async fn reencrypt(
+		&self,
+		data: SecretData,
+		targets: BTreeSet<SecretOwner>,
+	) -> Result<SecretData> {
 		ensure!(data.encrypted, "secret is not encrypted");
 		let mut cmd = self.cmd("fleet-install-secrets").await?;
 		cmd.arg("reencrypt").eqarg("--secret", data.to_string());
@@ -537,12 +573,25 @@
 	}
 }
 
+#[derive(Clone)]
 pub struct SharedSecretDefinition(Value);
 impl SharedSecretDefinition {
-	pub fn expected_owners(&self) -> Result<BTreeSet<String>> {
+	pub fn expected_owners(&self) -> Result<BTreeSet<SecretOwner>> {
 		let secret = &self.0;
 		Ok(nix_go_json!(secret.expectedOwners))
 	}
+	pub fn allow_different(&self) -> Result<bool> {
+		let secret = &self.0;
+		Ok(nix_go_json!(secret.allowDifferent))
+	}
+	pub fn regenerate_on_owner_added(&self) -> Result<bool> {
+		let secret = &self.0;
+		Ok(nix_go_json!(secret.regenerateOnOwnerAdded))
+	}
+	pub fn regenerate_on_owner_removed(&self) -> Result<bool> {
+		let secret = &self.0;
+		Ok(nix_go_json!(secret.regenerateOnOwnerRemoved))
+	}
 	pub fn generator(&self) -> Result<Value> {
 		let secret = &self.0;
 		Ok(nix_go!(secret.generator))
@@ -572,10 +621,10 @@
 			config: self.clone(),
 			name: "<virtual localhost>".to_owned(),
 			host_config: None,
-			nixos_config: OnceCell::new(),
-			nixos_unchecked_config: OnceCell::new(),
+			nixos_config: OnceLock::new(),
+			nixos_unchecked_config: OnceLock::new(),
 			groups: {
-				let cell = OnceCell::new();
+				let cell = OnceLock::new();
 				let _ = cell.set(vec![]);
 				cell
 			},
@@ -583,9 +632,9 @@
 
 			local: true,
 			session: OnceLock::new(),
-			deploy_kind: OnceCell::new(),
-			session_destination: OnceCell::new(),
-			legacy_ssh_store: OnceCell::new(),
+			deploy_kind: OnceLock::new(),
+			session_destination: OnceLock::new(),
+			legacy_ssh_store: OnceLock::new(),
 		}
 	}
 
@@ -597,17 +646,17 @@
 			config: self.clone(),
 			name: name.to_owned(),
 			host_config: Some(host_config),
-			nixos_config: OnceCell::new(),
-			nixos_unchecked_config: OnceCell::new(),
-			groups: OnceCell::new(),
+			nixos_config: OnceLock::new(),
+			nixos_unchecked_config: OnceLock::new(),
+			groups: OnceLock::new(),
 			pkgs_override: None,
 
 			// TODO: Remove with connectivit refactor
 			local: self.localhost == name,
 			session: OnceLock::new(),
-			deploy_kind: OnceCell::new(),
-			session_destination: OnceCell::new(),
-			legacy_ssh_store: OnceCell::new(),
+			deploy_kind: OnceLock::new(),
+			session_destination: OnceLock::new(),
+			legacy_ssh_store: OnceLock::new(),
 		})
 	}
 	pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
@@ -623,55 +672,6 @@
 	pub fn system_config(&self, host: &str) -> Result<Value> {
 		let fleet_field = &self.config_field;
 		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))
-	}
-
-	/// Shared secrets configured in fleet.nix or in flake
-	pub fn list_configured_shared(&self) -> Result<Vec<String>> {
-		let config_field = &self.config_field;
-		nix_go!(config_field.sharedSecrets).list_fields()
-	}
-	pub fn has_shared(&self, name: &str) -> bool {
-		let data = self.data();
-		data.secrets.contains(name)
-	}
-	pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {
-		let mut data = self.data_mut();
-		data.secrets.set_data(name, shared);
-	}
-	pub fn remove_shared(&self, secret: &str) {
-		let mut data = self.data_mut();
-		data.secrets.remove(secret);
-	}
-
-	pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {
-		let data = self.data_mut();
-		data.secrets.keys_for_owner(host).cloned().collect()
-	}
-	pub fn list_secrets(&self) -> Vec<String> {
-		let data = self.data_mut();
-		data.secrets.keys().cloned().collect()
-	}
-
-	pub fn has_secret(&self, host: &str, secret: &str) -> bool {
-		let data = self.data();
-		data.secrets.contains_for_owner(secret, host)
-	}
-	pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {
-		let mut data = self.data_mut();
-		data.secrets.set_single_data(secret, host, value);
-	}
-	pub fn remove_secret(&self, host: &str, secret: &str) {
-		let mut data = self.data_mut();
-		data.secrets.drop_owner_no_reencrypt(secret, host);
-	}
-
-	pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {
-		let data = self.data();
-		data.secrets.get_single(secret, host).cloned()
-	}
-	pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {
-		let data = self.data();
-		data.secrets.get(secret).cloned()
 	}
 
 	pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {
@@ -685,22 +685,9 @@
 		))))
 	}
 
-	// TODO: Should this be something modifiable from other processes?
-	// E.g terraform provider might want to update FleetData (e.g secrets),
-	// and current implementation assumes only one process holds current fleet.nix
-	// Given that it is no longer needs to be a file for nix evaluation,
-	// 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> {
-		self.data.lock().unwrap()
-	}
-	pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {
-		self.data.lock().unwrap()
-	}
 	pub fn save(&self) -> Result<()> {
 		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;
-		let data = nixlike::serialize(&self.data() as &FleetData)?;
+		let data = nixlike::serialize(&*self.data)?;
 		tempfile.write_all(
 			format!(
 				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -1,17 +1,17 @@
 use std::str::FromStr as _;
 
 use age::Recipient;
-use anyhow::{Result, anyhow};
+use anyhow::{Result, anyhow, bail};
 use futures::{StreamExt as _, TryStreamExt as _};
 use itertools::Itertools as _;
 use tracing::warn;
 
-use crate::host::Config;
+use crate::{fleetdata::SecretOwner, host::Config};
 
 impl Config {
-	pub fn cached_key(&self, host: &str) -> Option<String> {
-		let data = self.data();
-		let key = data.hosts.get(host).map(|h| &h.encryption_key);
+	fn cached_host_key(&self, host: &str) -> Option<String> {
+		let hosts = self.data.hosts.read().expect("no poisoning");
+		let key = hosts.get(host).map(|h| &h.encryption_key);
 		if let Some(key) = key
 			&& key.is_empty()
 		{
@@ -20,13 +20,13 @@
 		key.cloned()
 	}
 	pub fn update_key(&self, host: &str, key: String) {
-		let mut data = self.data_mut();
-		let host = data.hosts.entry(host.to_string()).or_default();
+		let mut hosts = self.data.hosts.write().expect("no poisoning");
+		let host = hosts.entry(host.to_string()).or_default();
 		host.encryption_key = key.trim().to_string();
 	}
 
-	pub async fn key(&self, host: &str) -> anyhow::Result<String> {
-		if let Some(key) = self.cached_key(host) {
+	pub async fn host_key(&self, host: &str) -> anyhow::Result<String> {
+		if let Some(key) = self.cached_host_key(host) {
 			Ok(key)
 		} else {
 			warn!("Loading key for {}", host);
@@ -38,18 +38,24 @@
 			Ok(key)
 		}
 	}
+	pub async fn key(&self, owner: &SecretOwner) -> anyhow::Result<String> {
+		if let Some(host) = owner.as_host() {
+			self.host_key(host).await
+		} else {
+			bail!("only host keys supported for now")
+		}
+	}
 	/// Insecure, requires root
-	pub async fn recipient(&self, host: &str) -> anyhow::Result<Box<dyn Recipient>> {
+	pub async fn recipient(&self, host: &SecretOwner) -> anyhow::Result<Box<dyn Recipient>> {
 		let key = self.key(host).await?;
 		age::ssh::Recipient::from_str(&key)
 			.map_err(|e| anyhow!("parse recipient error: {e:?}"))
 			.map(|v| Box::new(v) as Box<dyn Recipient>)
 	}
 
-	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {
-		let hosts = self.expand_owner_set(hosts)?;
+	pub async fn recipients(&self, hosts: Vec<SecretOwner>) -> Result<Vec<Box<dyn Recipient>>> {
 		futures::stream::iter(hosts.iter())
-			.then(|m| self.recipient(m.as_ref()))
+			.then(|m| self.recipient(m))
 			.try_collect::<Vec<_>>()
 			.await
 	}
@@ -58,9 +64,8 @@
 	pub async fn orphaned_data(&self) -> Result<Vec<String>> {
 		let mut out = Vec::new();
 		let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
-		for hostname in self
-			.data()
-			.hosts
+		let hosts = self.data.hosts.read().expect("no poisoning");
+		for hostname in hosts
 			.iter()
 			.filter(|(_, host)| !host.encryption_key.is_empty())
 			.map(|(n, _)| n)
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -5,5 +5,4 @@
 mod keys;
 pub mod opts;
 pub mod primops;
-pub mod secret;
 pub mod secret_storage;
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -1,5 +1,5 @@
 use std::{
-	collections::BTreeMap,
+	collections::{BTreeMap, BTreeSet},
 	env::current_dir,
 	ffi::OsString,
 	str::FromStr,
@@ -7,6 +7,7 @@
 };
 
 use anyhow::{Context, Result, bail};
+use chrono::Utc;
 use nix_eval::{
 	FetchSettings, FlakeLockFlags, FlakeReference, FlakeReferenceParseFlags, FlakeSettings, Value,
 	gc_now, nix_go, util::assert_warn,
@@ -212,7 +213,7 @@
 		}
 		let bytes =
 			std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
-		let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));
+		let data = Arc::new(FleetData::from_str(&bytes)?);
 
 		init_primops();
 
@@ -265,6 +266,10 @@
 			gc_now();
 		}
 		let config = Config(Arc::new(FleetConfigInternals {
+			// TODO: Load from somewhere
+			prefer_identities: BTreeSet::new(),
+			now: Utc::now(),
+
 			directory,
 			data,
 			flake_outputs: flake,
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -4,19 +4,16 @@
 use anyhow::{Context, bail, ensure};
 use fleet_shared::SecretData;
 use itertools::Itertools;
-use nix_eval::{NativeFn, Value, nix_go, nix_go_json};
+use nix_eval::{NativeFn, Value, await_in_nix, nix_go, nix_go_json};
 use serde::Deserialize;
 use tracing::{info, warn};
 
 use crate::fleetdata::{
 	Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
+	RegenerationConstraints, SecretOwner,
 };
 use crate::host::{Config, ConfigHost};
-use crate::secret::{RegenerationReason, secret_needs_regeneration};
 use anyhow::{Result, anyhow};
-
-#[derive(thiserror::Error, Debug)]
-enum Error {}
 
 pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
 
@@ -28,7 +25,6 @@
 }
 
 pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {
-	info!("get pkgs");
 	let pkgs = host_on.pkgs()?;
 	let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
 	let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
@@ -57,6 +53,31 @@
 	Ok(default_generator_drv)
 }
 
+fn secret_to_parts(
+	secret_name: &str,
+	secret: &BTreeMap<String, FleetSecretPart>,
+	expected: &BTreeMap<String, GeneratorPart>,
+) -> Value {
+	let mut out = HashMap::new();
+	for (part_name, part) in secret {
+		if !expected.contains_key(part_name) {
+			warn!(
+				"secret {secret_name} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix"
+			);
+			continue;
+		};
+		out.insert(
+			part_name.as_str(),
+			Value::new_attrs(HashMap::from_iter([(
+				"raw",
+				Value::new_str(&part.raw.to_string()),
+			)])),
+		);
+	}
+
+	Value::new_attrs(out)
+}
+
 pub async fn generate(
 	config: &Config,
 	expectations: Expectations,
@@ -76,9 +97,12 @@
 			} else {
 				config.local_host()
 			};
-			let pkgs_and_generators =
-				get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())
-					.context("failed to get pkgs for target host")?;
+			let mut recipients = Vec::new();
+			for owner in &expectations.owners {
+				recipients.push(config.key(owner).await?);
+			}
+			let pkgs_and_generators = get_pkgs_and_generators(&host_on, recipients)
+				.context("failed to get pkgs for target host")?;
 			let generator = call_package(config, &pkgs_and_generators, generator)
 				.context("failed to evaluate generator for target host")?;
 
@@ -147,15 +171,8 @@
 				generation_data: expectations.generation_data.clone(),
 			};
 
-			let new_data = FleetSecretDistribution {
-				secret: new_data,
-				owners: expectations.owners.clone(),
-				_deprecated_managed: true,
-			};
-
-			if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {
-				bail!("newly generated secret needs to be regenerated: {reason}")
-			}
+			let new_data =
+				FleetSecretDistribution::new(expectations.owners.clone(), new_data, config.now);
 
 			Ok(new_data)
 		}
@@ -166,18 +183,41 @@
 }
 
 pub fn init_primops() {
-	info!("initializing primops");
 	NativeFn::new(
+		c"__fleetEnsureHostSecrets",
+		c"Ensure no extra secrets are stored for the host, pruning unknown",
+		[c"host", c"expectedNonshared", c"expectedShared", c"rest"],
+		|_es, [host, expected_nonshared, expected_shared, rest]| {
+			let host = SecretOwner::host(host.to_string()?);
+			let expected_nonshared: BTreeSet<String> = expected_nonshared.as_json()?;
+			let expected_shared: BTreeSet<String> = expected_shared.as_json()?;
+
+			let mut expected = expected_nonshared;
+			expected.extend(expected_shared);
+
+			let config = PRIMOPS_DATA
+				.get()
+				.expect("primops data should be set on init");
+
+			config
+				.data
+				.secrets
+				.write()
+				.expect("no poisoning")
+				.prune_host(&host, expected);
+
+			Ok(rest.clone())
+		},
+	)
+	.register();
+	NativeFn::new(
 		c"__fleetEnsureHostSecret",
 		c"Ensure secret existence for a host, regenerating it in case of some mismatch",
 		[c"host", c"secret", c"generator"],
 		|es, [host, secret, generator]| {
-			info!("get host");
-			let host = host.to_string()?;
-			info!("get secret");
+			let host = SecretOwner::host(&host.to_string()?);
 			let secret = secret.to_string()?;
 
-			info!("get config");
 			let config = PRIMOPS_DATA
 				.get()
 				.expect("primops data should be set on init");
@@ -193,50 +233,101 @@
 
 				ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
 
-				(true, shared_def.generator()?, expected_owners)
+				(Some(shared_def.clone()), shared_def.generator()?, expected_owners)
 			} else {
 				if shared_def.is_some() {
 					bail!("hosts can only have their own generators for non-shared secrets, either set host secret generator to \"shared\", or remove shared secret generator at fleetConfiguration.secrets.{secret}.generator")
 				}
 
-				(false, generator.clone(), BTreeSet::from_iter([host.clone()]))
+				(None, generator.clone(), BTreeSet::from_iter([host.clone()]))
 			};
 
-			let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;
-			let expectations = Expectations {
+			let default_generator_drv = get_default_generator_drv(config, &generator)?;
+			let mut expectations = Expectations {
 				parts: nix_go_json!(default_generator_drv.parts),
 				generation_data: nix_go_json!(default_generator_drv.generationData),
-				owners: expected_owners,
+				owners: expected_owners.clone(),
+			};
+			let constraints = if let Some(shared) = &shared{
+				RegenerationConstraints {
+					allow_different: nix_go_json!(default_generator_drv.allowDifferent) && shared.allow_different()?,
+					regenerate_on_owner_added: shared.regenerate_on_owner_added()?,
+					regenerate_on_owner_removed: shared.regenerate_on_owner_added()?,
+				}
+			} else {
+				RegenerationConstraints::host_personal()
 			};
 
-			let reason: RegenerationReason = 'regenerate: {
-				let Some(existing) = config
-					.host_secret(&host, &secret) else {
-					break 'regenerate RegenerationReason::Missing;
+			let mut secrets = config.data.secrets.write().expect("no poisoning");
+			let dists = secrets.get_or_create(&secret);
+
+				if shared.is_some() {
+					dists.prune_shared(&expected_owners, !constraints.allow_different, &expectations.parts, &expectations.generation_data, constraints.regenerate_on_owner_removed, constraints.regenerate_on_owner_added, &config.prefer_identities, config.now);
+				} else {
+					dists.prune_host(host.clone(), &expectations.parts, &expectations.generation_data, config.now);
+				};
+
+				if let Some(dist) = dists.get(&host) {
+					return Ok(secret_to_parts(&secret, &dist.secret.parts, &expectations.parts));
 				};
-				if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {
-					break 'regenerate reason;
+
+				let mut reencrypt_targets = expectations.owners.clone();
+				for dist in dists.distributions() {
+					for own in dist.owners() {
+						reencrypt_targets.remove(own);
+					}
 				}
+				if !constraints.regenerate_on_owner_added {
+					if let Some(unpruned) = dists.try_unprune(host.clone()) {
+						return Ok(secret_to_parts(&secret, &unpruned.secret.parts, &expectations.parts));
+					} else if let Some(best) = dists.best_distribution_for_reencryption(&config.prefer_identities) {
+						let new_owners = reencrypt_targets.clone();
+						let mut reencrypt_targets = reencrypt_targets;
+						reencrypt_targets.extend(best.owners().cloned());
 
-				let mut parts = expectations.parts.clone();
+						let mut preferred = best.owners().collect_vec();
+						preferred.sort_by_key(|v| !config.prefer_identities.contains(*v));
+
+						warn!("reencrypting secret {secret} as it is missing for host {host}");
 
-				let mut out = HashMap::new();
-				for (part_name, part) in &existing.secret.parts {
-					let Some(definition) = parts.remove(part_name) else {
-						warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
-						continue;
+						for owner in preferred {
+							if let Some(hostname) = owner.as_host() && let Ok(host) = config.host(hostname) {
+								let best = best.clone();
+								let reencrypt_targets = reencrypt_targets.clone();
+								let reencrypted = match await_in_nix(async move {
+										host.reencrypt_distribution(&best, reencrypt_targets.clone(), config.now).await
+								}) {
+									Ok(r) => r,
+									Err(e) => {
+										warn!("reencryption failed on {hostname}: {e:?}");
+										continue;
+									}
+								};
+								dists.extend(reencrypted.clone(), format!("secret was reencrypted to extend with new owners: {new_owners:?}"));
+								return Ok(secret_to_parts(&secret, &reencrypted.secret.parts, &expectations.parts));
+							};
+						}
+						warn!("failed to reencrypt using any host")
 					};
-					assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");
-					out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
-				}
-				assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");
+				};
 
-				return Ok(Value::new_attrs(out))
-			};
+			if constraints.allow_different {
+				for dist in dists.distributions() {
+					for own in dist.owners() {
+						expectations.owners.remove(own);
+					}
+				}
+			}
+			info!("secret {secret} is being generated for {:?}", expectations.owners);
 
-			todo!()
+			let expectations_ = expectations.clone();
+			let generated = await_in_nix(async move {
+				generate(config, expectations_, &generator, &default_generator_drv).await
+			})?;
 
+			dists.extend(generated.clone(), format!("secret was generated"));
 
+			return Ok(secret_to_parts(&secret, &generated.secret.parts, &expectations.parts));
 		},
 	)
 	.register();
deletedcrates/fleet-base/src/secret.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/secret.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-
-use chrono::{DateTime, Utc};
-
-use crate::fleetdata::{Expectations, FleetSecretData, FleetSecretDistribution, GeneratorPart};
-
-#[derive(thiserror::Error, Debug)]
-pub enum RegenerationReason {
-	#[error("owners added: {0:?}")]
-	OwnersAdded(BTreeSet<String>),
-	#[error("owners added: {0:?}")]
-	OwnersRemoved(BTreeSet<String>),
-	#[error("unexpected generation data, expected: {expected:?}, found: {found:?}")]
-	GenerationData {
-		expected: serde_json::Value,
-		found: serde_json::Value,
-	},
-	#[error("unexpected part list, expected: {expected:?}, found: {found:?}")]
-	PartList {
-		expected: BTreeSet<String>,
-		found: BTreeSet<String>,
-	},
-	#[error("part {0} is expected to be encrypted")]
-	ExpectedPrivate(String),
-	#[error("part {0} is not expected to be encrypted")]
-	ExpectedPublic(String),
-	#[error("secret is expired at {0}")]
-	Expired(DateTime<Utc>),
-
-	#[error("secret is not generated for this host")]
-	Missing,
-}
-
-pub fn secret_needs_regeneration(
-	secret: &FleetSecretDistribution,
-	expectations: &Expectations,
-) -> Option<RegenerationReason> {
-	let added: BTreeSet<String> = expectations
-		.owners
-		.difference(&secret.owners)
-		.cloned()
-		.collect();
-	if !added.is_empty() {
-		return Some(RegenerationReason::OwnersAdded(added));
-	}
-
-	let removed: BTreeSet<String> = secret
-		.owners
-		.difference(&expectations.owners)
-		.cloned()
-		.collect();
-	if !removed.is_empty() {
-		return Some(RegenerationReason::OwnersRemoved(removed));
-	}
-
-	if secret.secret.generation_data != expectations.generation_data {
-		return Some(RegenerationReason::GenerationData {
-			expected: expectations.generation_data.clone(),
-			found: secret.secret.generation_data.clone(),
-		});
-	}
-
-	let expected: BTreeSet<String> = expectations.parts.keys().cloned().collect();
-	let found: BTreeSet<String> = secret.secret.parts.keys().cloned().collect();
-
-	if found != expected {
-		return Some(RegenerationReason::PartList { expected, found });
-	}
-
-	for (name, value) in secret.secret.parts.iter() {
-		let expectation = expectations
-			.parts
-			.get(name)
-			.expect("found == expected checked");
-		if value.raw.encrypted {
-			if !expectation.encrypted {
-				return Some(RegenerationReason::ExpectedPrivate(name.clone()));
-			}
-		} else if expectation.encrypted {
-			return Some(RegenerationReason::ExpectedPublic(name.clone()));
-		}
-	}
-
-	if let Some(expiration) = secret.secret.expires_at {
-		// TODO: Leeway?
-		if expiration < Utc::now() {
-			return Some(RegenerationReason::Expired(expiration));
-		}
-	}
-
-	None
-}
modifiedcrates/fleet-shared/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-shared/Cargo.toml
+++ b/crates/fleet-shared/Cargo.toml
@@ -8,4 +8,3 @@
 base64 = "0.22.1"
 serde = "1.0.219"
 unicode_categories = "0.1.1"
-z85 = "3.0.6"
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -1,5 +1,4 @@
 use std::{
-	collections::BTreeMap,
 	fmt::{self, Display},
 	str::FromStr,
 };
@@ -15,7 +14,6 @@
 }
 
 const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";
-const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";
 // Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.
 const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";
 const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";
@@ -54,18 +52,12 @@
 			STANDARD_NO_PAD
 				.decode(unprefixed.replace(['\n', '\t', ' '], ""))
 				.map_err(|e| format!("base64-encoded failed: {e}"))?
-		} else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {
-			z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))
-				.map_err(|e| format!("z85-encoded failed: {e}"))?
 		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {
 			unprefixed.as_bytes().to_owned()
 		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {
 			unprefixed.as_bytes().to_owned()
 		} else {
-			let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");
-			return Err(format!(
-				"unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"
-			));
+			return Err(format!("unknown secret encoding"));
 		};
 		Ok(Self { data, encrypted })
 	}
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -18,6 +18,7 @@
 test-log = { version = "0.2.18", features = ["trace"] }
 tracing-indicatif = { version = "0.3.13", optional = true }
 vte = { version = "0.15.0", features = ["ansi"] }
+tokio.workspace = true
 
 [build-dependencies]
 bindgen = "0.72.0"
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
before · crates/nix-eval/src/lib.rs
1use std::borrow::Cow;2use std::cell::RefCell;3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};4use std::ptr::{null, null_mut};5use std::sync::LazyLock;6use std::{array, fmt, slice};7use std::{collections::HashMap, path::PathBuf};89use anyhow::{Context, anyhow, bail};10use itertools::Itertools;11use serde::Serialize;12use serde::de::DeserializeOwned;1314pub use anyhow::Result;15use tracing::{info, instrument, warn};1617use self::logging::{ErrorInfoBuilder, nix_logging_cxx};18use self::nix_cxx::set_fetcher_setting;19use self::nix_raw::{20	BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,21	GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base,22	GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,23	PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,24	bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,25	clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,26	err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,27	eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,28	expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,29	flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,30	flake_reference_and_fragment_from_string, flake_reference_parse_flags,31	flake_reference_parse_flags_free, flake_reference_parse_flags_new,32	flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,33	flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size,34	get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int,35	init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free,36	list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs,37	make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,38	realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,39	realised_string_get_store_path, realised_string_get_store_path_count, register_primop,40	set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,41	store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,42};4344// Contains macros helpers45pub mod logging;46#[doc(hidden)]47pub mod macros;48pub mod util;4950#[allow(51	non_upper_case_globals,52	non_camel_case_types,53	non_snake_case,54	dead_code55)]56mod nix_raw {57	include!(concat!(env!("OUT_DIR"), "/bindings.rs"));58}59#[cxx::bridge]60pub mod nix_cxx {61	unsafe extern "C++" {62		type nix_fetchers_settings;63		include!("nix-eval/src/lib.hh");6465		#[allow(clippy::missing_safety_doc)]66		unsafe fn set_fetcher_setting(67			settings: *mut nix_fetchers_settings,68			setting: *const c_char,69			value: *const c_char,70		);71	}72}7374#[derive(Debug, PartialEq, Eq)]75pub enum NixType {76	Thunk,77	Int,78	Float,79	Bool,80	String,81	Path,82	Null,83	Attrs,84	List,85	Function,86	External,87}88impl NixType {89	fn from_int(c: c_uint) -> Self {90		match c {91			0 => Self::Thunk,92			1 => Self::Int,93			2 => Self::Float,94			3 => Self::Bool,95			4 => Self::String,96			5 => Self::Path,97			6 => Self::Null,98			7 => Self::Attrs,99			8 => Self::List,100			9 => Self::Function,101			10 => Self::External,102			_ => unreachable!("unknown nix type: {c}"),103		}104	}105}106107enum FunctorKind {108	Function,109	Functor,110}111112#[derive(Debug)]113#[repr(i32)]114pub enum NixErrorKind {115	Unknown = err_NIX_ERR_UNKNOWN,116	Overflow = err_NIX_ERR_OVERFLOW,117	Key = err_NIX_ERR_KEY,118	Generic = err_NIX_ERR_NIX_ERROR,119}120impl NixErrorKind {121	fn from_int(v: c_int) -> Option<Self> {122		Some(match v {123			0 => return None,124			nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,125			nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,126			nix_raw::err_NIX_ERR_KEY => Self::Key,127			nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,128			_ => {129				debug_assert!(false, "unexpected nix error kind: {v}");130				Self::Unknown131			}132		})133	}134}135136pub fn gc_now() {137	unsafe { gc_now_raw() };138}139140pub fn gc_register_my_thread() {141	assert_eq!(unsafe { GC_thread_is_registered() }, 0);142143	let mut sb = GC_stack_base {144		mem_base: null_mut(),145	};146	let r = unsafe { GC_get_stack_base(&mut sb) };147	if r as u32 != GC_SUCCESS {148		panic!("failed to get thread stack base");149	}150	unsafe { GC_register_my_thread(&sb) };151}152pub fn gc_unregister_my_thread() {153	assert_eq!(unsafe { GC_thread_is_registered() }, 1);154155	unsafe { GC_unregister_my_thread() };156}157158pub struct ThreadRegisterGuard {}159impl ThreadRegisterGuard {160	#[allow(clippy::new_without_default)]161	pub fn new() -> Self {162		gc_register_my_thread();163		Self {}164	}165}166impl Drop for ThreadRegisterGuard {167	fn drop(&mut self) {168		gc_unregister_my_thread();169	}170}171172#[repr(transparent)]173pub struct NixContext(*mut c_context);174impl NixContext {175	pub fn set_err(&mut self, err: NixErrorKind, msg: &CStr) {176		unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };177	}178	pub fn new() -> Self {179		let ctx = unsafe { c_context_create() };180		Self(ctx)181	}182	fn error_kind(&self) -> Option<NixErrorKind> {183		let code = unsafe { err_code(self.0) };184		NixErrorKind::from_int(code)185	}186	fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {187		if let NixErrorKind::Generic = self.error_kind()? {188			let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };189			let mut err_out = String::new();190			unsafe {191				err_info_msg(192					null_mut(),193					self.0,194					Some(copy_nix_str),195					(&raw mut err_out).cast(),196				)197			};198			return Some((Cow::Owned(err_out), Some(ei)));199		};200201		// TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,202		// but it looks ugly203		let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };204		Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))205	}206	fn clean_err(&mut self) {207		unsafe {208			clear_err(self.0);209		}210	}211212	fn bail_if_error(&self) -> Result<()> {213		if let Some((err, stack)) = self.error() {214			let mut e = Err(anyhow!("{err}"));215			if let Some(stack) = stack {216				for ele in stack.stack_frames {217					e = e.with_context(|| {218						if ele.pos.is_empty() {219							ele.msg220						} else {221							format!("{} at {}", ele.msg, ele.pos)222						}223					})224				}225			}226			return e.context("<nix frames>");227		};228		Ok(())229	}230231	fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {232		self.clean_err();233		let o = f(self.0);234		self.bail_if_error()?;235		self.clean_err();236		Ok(o)237	}238}239240impl Default for NixContext {241	fn default() -> Self {242		Self::new()243	}244}245impl Drop for NixContext {246	fn drop(&mut self) {247		unsafe {248			c_context_free(self.0);249		}250	}251}252struct GlobalState {253	// Store should be valid as long as EvalState is valid254	#[allow(dead_code)]255	store: Store,256	state: EvalState,257}258impl GlobalState {259	fn new() -> Result<Self> {260		let mut ctx = NixContext::new();261		let store = ctx262			.run_in_context(|c| unsafe { store_open(c, c"auto".as_ptr(), null_mut()) })263			.map(Store)?;264265		let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;266		ctx.run_in_context(|c| unsafe { eval_state_builder_load(c, builder) })?;267		ctx.run_in_context(|c| unsafe {268			eval_state_builder_set_eval_setting(269				c,270				builder,271				c"lazy-trees".as_ptr(),272				c"true".as_ptr(),273			)274		})?;275		ctx.run_in_context(|c| unsafe {276			eval_state_builder_set_eval_setting(277				c,278				builder,279				c"lazy-locks".as_ptr(),280				c"true".as_ptr(),281			)282		})?;283		let state = ctx284			.run_in_context(|c| unsafe { eval_state_build(c, builder) })285			.map(EvalState)?;286287		Ok(Self { store, state })288	}289}290291struct ThreadState {292	ctx: NixContext,293}294impl ThreadState {295	fn new() -> Result<Self> {296		let ctx = NixContext::new();297298		Ok(Self { ctx })299	}300}301302static GLOBAL_STATE: LazyLock<GlobalState> = LazyLock::new(|| {303	info!("initializing nix global state");304	GlobalState::new().expect("global state init shouldn't fail")305});306307thread_local! {308	static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));309}310fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {311	let global = &GLOBAL_STATE.state;312	let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));313	let mut ctx = NixContext(ctx);314	let v = ctx.run_in_context(|c| f(c, state));315	// It is reused for thread316	std::mem::forget(ctx);317	v318}319320pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> {321	with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())322}323324pub struct FetchSettings(*mut fetchers_settings);325impl FetchSettings {326	pub fn new() -> Self {327		Self::try_new().expect("allocation should not fail")328	}329	fn try_new() -> Result<Self> {330		with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)331	}332	pub fn set(&mut self, setting: &CStr, value: &CStr) {333		unsafe {334			set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());335		};336	}337}338unsafe impl Send for FetchSettings {}339unsafe impl Sync for FetchSettings {}340341impl Default for FetchSettings {342	fn default() -> Self {343		Self::new()344	}345}346347impl Drop for FetchSettings {348	fn drop(&mut self) {349		unsafe { fetchers_settings_free(self.0) };350	}351}352pub struct FlakeSettings(*mut flake_settings);353impl FlakeSettings {354	pub fn new() -> Result<Self> {355		with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)356	}357}358unsafe impl Send for FlakeSettings {}359unsafe impl Sync for FlakeSettings {}360impl Drop for FlakeSettings {361	fn drop(&mut self) {362		unsafe {363			flake_settings_free(self.0);364		}365	}366}367368pub struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);369impl FlakeReferenceParseFlags {370	pub fn new(settings: &FlakeSettings) -> Result<Self> {371		with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })372			.map(Self)373	}374	pub fn set_base_dir(&mut self, dir: &str) -> Result<()> {375		with_default_context(|c, _| {376			unsafe {377				flake_reference_parse_flags_set_base_directory(378					c,379					self.0,380					dir.as_ptr().cast(),381					dir.len(),382				)383			};384		})385	}386}387impl Drop for FlakeReferenceParseFlags {388	fn drop(&mut self) {389		unsafe {390			flake_reference_parse_flags_free(self.0);391		}392	}393}394pub struct FlakeLockFlags(*mut flake_lock_flags);395impl FlakeLockFlags {396	pub fn new(settings: &FlakeSettings) -> Result<Self> {397		let o = with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) })398			.map(Self)?;399		// with_default_context(|c, _| unsafe { flake_lock_flags_set_mode_virtual(c, o.0) })?;400401		Ok(o)402	}403}404impl Drop for FlakeLockFlags {405	fn drop(&mut self) {406		unsafe {407			flake_lock_flags_free(self.0);408		}409	}410}411412unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {413	let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };414	let s = std::str::from_utf8(s).expect("c string has invalid utf-8");415	unsafe { *user_data.cast::<String>() = s.to_owned() };416}417418struct Store(*mut c_store);419unsafe impl Send for Store {}420unsafe impl Sync for Store {}421422impl Store {423	fn parse_path(&self, path: &CStr) -> Result<StorePath> {424		with_default_context(|c, _| {425			StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })426		})427	}428}429430#[repr(transparent)]431pub struct EvalState(*mut c_eval_state);432unsafe impl Send for EvalState {}433unsafe impl Sync for EvalState {}434435impl Drop for EvalState {436	fn drop(&mut self) {437		unsafe {438			state_free(self.0);439		}440	}441}442443pub struct FlakeReference(*mut flake_reference);444impl FlakeReference {445	#[instrument(name = "new-flake-reference", skip(flake, parse, fetch))]446	pub fn new(447		s: &str,448		flake: &FlakeSettings,449		parse: &FlakeReferenceParseFlags,450		fetch: &FetchSettings,451	) -> Result<(Self, String)> {452		let mut out = null_mut();453		let mut fragment = String::new();454		// let fetch_settings = fetcher_settings;455		with_default_context(|c, _| unsafe {456			flake_reference_and_fragment_from_string(457				c,458				fetch.0,459				flake.0,460				parse.0,461				s.as_ptr().cast(),462				s.len(),463				&mut out,464				Some(copy_nix_str),465				(&raw mut fragment).cast(),466			)467		})?;468		assert!(!out.is_null());469470		Ok((Self(out), fragment))471	}472	#[instrument(name = "lock-flake", skip(self, fetch, flake, lock))]473	pub fn lock(474		&mut self,475		fetch: &FetchSettings,476		flake: &FlakeSettings,477		lock: &FlakeLockFlags,478	) -> Result<LockedFlake> {479		with_default_context(|c, es| unsafe { flake_lock(c, fetch.0, flake.0, es, lock.0, self.0) })480			.map(LockedFlake)481	}482}483unsafe impl Send for FlakeReference {}484unsafe impl Sync for FlakeReference {}485486pub struct LockedFlake(*mut locked_flake);487impl LockedFlake {488	pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {489		with_default_context(|c, es| unsafe {490			locked_flake_get_output_attrs(c, settings.0, es, self.0)491		})492		.map(Value)493	}494}495unsafe impl Send for LockedFlake {}496unsafe impl Sync for LockedFlake {}497impl Drop for LockedFlake {498	fn drop(&mut self) {499		unsafe {500			locked_flake_free(self.0);501		};502	}503}504505type FieldName = [u8; 64];506fn init_field_name(v: &str) -> FieldName {507	let mut f = [0; 64];508	assert!(v.len() < 64, "max field name is 63 chars");509	assert!(510		v.bytes().all(|v| v != 0),511		"nul bytes are unsupported in field name"512	);513	f[0..v.len()].copy_from_slice(v.as_bytes());514	f515}516517pub struct RealisedString(*mut realised_string);518impl fmt::Debug for RealisedString {519	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {520		self.as_str().fmt(f)521	}522}523524impl RealisedString {525	pub fn as_str(&self) -> &str {526		let len = unsafe { realised_string_get_buffer_size(self.0) };527		let data: *const u8 = unsafe { realised_string_get_buffer_start(self.0) }.cast();528		let data = unsafe { slice::from_raw_parts(data, len) };529		std::str::from_utf8(data).expect("non-utf8 strings not supported")530	}531	pub fn path_count(&self) -> usize {532		unsafe { realised_string_get_store_path_count(self.0) }533	}534	pub fn path(&self, i: usize) -> String {535		assert!(i < self.path_count());536		let path = unsafe { realised_string_get_store_path(self.0, i) };537		let mut err_out = String::new();538		unsafe { store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };539		err_out540	}541}542543unsafe impl Send for RealisedString {}544impl Drop for RealisedString {545	fn drop(&mut self) {546		unsafe { realised_string_free(self.0) }547	}548}549550#[repr(transparent)]551pub struct Value(*mut value);552553unsafe impl Send for Value {}554unsafe impl Sync for Value {}555556pub trait AsFieldName {557	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;558	fn to_field_name(&self) -> Result<String>;559}560impl AsFieldName for Value {561	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {562		let f = self.to_string()?;563		v(init_field_name(&f))564	}565	fn to_field_name(&self) -> Result<String> {566		self.to_string()567	}568}569impl<E> AsFieldName for E570where571	E: AsRef<str>,572{573	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {574		let f = self.as_ref();575		v(init_field_name(f))576	}577	fn to_field_name(&self) -> Result<String> {578		Ok(self.as_ref().to_owned())579	}580}581582struct AttrsBuilder(*mut c_bindings_builder);583impl AttrsBuilder {584	fn new(capacity: usize) -> Self {585		with_default_context(|c, es| unsafe { make_bindings_builder(c, es, capacity) })586			.map(Self)587			.expect("alloc should not fail")588	}589	fn insert(&mut self, k: &impl AsFieldName, v: Value) {590		k.as_field_name(|name| {591			with_default_context(|c, _| unsafe {592				bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0);593				// bindings_builder_insert doesn't do incref594			})595		})596		.expect("builder insert shouldn't fail");597	}598}599impl Drop for AttrsBuilder {600	fn drop(&mut self) {601		unsafe { bindings_builder_free(self.0) };602	}603}604605struct ListBuilder(*mut c_list_builder, c_uint);606impl ListBuilder {607	fn new(capacity: usize) -> Self {608		with_default_context(|c, es| unsafe { make_list_builder(c, es, capacity) })609			.map(|l| Self(l, 0))610			.expect("alloc should not fail")611	}612}613impl ListBuilder {614	fn push(&mut self, v: Value) {615		with_default_context(|c, _| unsafe {616			list_builder_insert(617				c,618				self.0,619				{620					let v = self.1;621					self.1 += 1;622					v623				},624				v.0,625			)626		})627		.expect("list insert shouldn't fail");628	}629}630impl Drop for ListBuilder {631	fn drop(&mut self) {632		unsafe { list_builder_free(self.0) };633	}634}635636impl Value {637	pub fn new_primop(v: NativeFn) -> Self {638		let out = Self::new_uninit();639		with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) })640			.expect("primop initialization should not fail");641		out642	}643	pub fn new_attrs(v: HashMap<&str, Value>) -> Self {644		let out = Self::new_uninit();645		let mut b = AttrsBuilder::new(v.len());646		for (k, v) in v {647			b.insert(&k, v);648		}649		with_default_context(|c, _| unsafe { make_attrs(c, out.0, b.0) })650			.expect("attrs initialization should not fail");651652		out653	}654	fn new_list<T: Into<Self>>(v: Vec<T>) -> Self {655		let out = Self::new_uninit();656		let mut b = ListBuilder::new(v.len());657		for v in v {658			b.push(v.into());659		}660		with_default_context(|c, _| unsafe { make_list(c, b.0, out.0) })661			.expect("list initialization should not fail");662663		out664	}665	fn new_uninit() -> Self {666		let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })667			.expect("value allocation should not fail");668		Self(out)669	}670	pub fn new_str(v: &str) -> Self {671		let s = CString::new(v).expect("string should not contain NULs");672		let out = Self::new_uninit();673		// String is copied, `s` is free to be dropped674		with_default_context(|c, _| unsafe { init_string(c, out.0, s.as_ptr()) })675			.expect("string initialization should not fail");676		out677	}678	pub fn new_int(i: i64) -> Self {679		let out = Self::new_uninit();680		with_default_context(|c, _| unsafe { init_int(c, out.0, i) })681			.expect("int initialization should not fail");682		out683	}684	pub fn new_bool(v: bool) -> Self {685		let out = Self::new_uninit();686		with_default_context(|c, _| unsafe { init_bool(c, out.0, v) })687			.expect("bool initialization should not fail");688		out689	}690	// TODO: As far as I can see, there is no way to get Thunks from nix public C api, so this function is useless691	// fn force(&mut self, st: &mut EvalState) -> Result<()> {692	// 	with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;693	// 	Ok(())694	// }695	pub fn type_of(&self) -> NixType {696		let ty = with_default_context(|c, _| unsafe { get_type(c, self.0) })697			.expect("get_type should not fail");698		NixType::from_int(ty)699	}700	fn builtin_to_string(&self) -> Result<Self> {701		let builtin = Self::eval("builtins.toString")?;702		builtin.call(self.clone())703	}704	fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {705		with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;706		Ok(())707	}708	pub fn to_string(&self) -> Result<String> {709		let ty = self.type_of();710		if !matches!(ty, NixType::String) {711			bail!("unexpected type: {ty:?}, expected string");712		}713		let mut str_out = String::new();714		with_default_context(|c, _| unsafe {715			get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())716		})?;717718		Ok(str_out)719	}720	pub fn to_realised_string(&self) -> Result<RealisedString> {721		with_default_context(|c, es| unsafe { string_realise(c, es, self.0, false) })722			.map(RealisedString)723724		// let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };725		// for i in 0..store_paths {726		// 	let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };727		// 	nix_raw::store_path_name(store_path, callback, user_data);728		// }729		// dbg!(store_paths);730		// todo!();731	}732733	pub fn has_field(&self, field: &str) -> Result<bool> {734		if !matches!(self.type_of(), NixType::Attrs) {735			bail!("invalid type: expected attrs");736		}737738		let f = init_field_name(field);739		with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })740	}741	// pub fn derivation_path(&self) {742	// 	nix_raw::real743	// }744	pub fn list_fields(&self) -> Result<Vec<String>> {745		if !matches!(self.type_of(), NixType::Attrs) {746			bail!("invalid type: expected attrs");747		}748749		let len = with_default_context(|c, _| unsafe { get_attrs_size(c, self.0) })?;750		let mut out = Vec::with_capacity(len as usize);751752		for i in 0..len {753			let name =754				with_default_context(|c, es| unsafe { get_attr_name_byidx(c, self.0, es, i) })?;755			let c = unsafe { CStr::from_ptr(name) };756			out.push(c.to_str().expect("nix field names are utf-8").to_owned());757		}758		Ok(out)759	}760	pub fn get_elem(&self, v: usize) -> Result<Self> {761		if !matches!(self.type_of(), NixType::List) {762			bail!("invalid type: expected list");763		}764		let len = with_default_context(|c, _| unsafe { get_list_size(c, self.0) })? as usize;765		if v >= len {766			bail!("oob list get: {v} >= {len}");767		}768769		with_default_context(|c, es| unsafe { get_list_byidx(c, self.0, es, v as u32) }).map(Self)770	}771	pub fn attrs_update(self, other: Value /*, ignore_errors: bool*/) -> Result<Self> {772		let attrs_update_fn = Self::eval("a: b: a // b")?;773774		attrs_update_fn775			.call(self)?776			.call(other)777			.context("attrs update")778	}779	pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {780		if !matches!(self.type_of(), NixType::Attrs) {781			bail!("invalid type: expected attrs");782		}783784		name.as_field_name(|name| {785			with_default_context(|c, es| unsafe {786				get_attr_byname(c, self.0, es, name.as_ptr().cast())787			})788			.map(Self)789		})790		.with_context(|| format!("getting field {:?}", name.to_field_name()))791	}792	pub fn call(&self, v: Value) -> Result<Self> {793		let kind = self794			.functor_kind()795			.ok_or_else(|| anyhow!("can only call function or functor"))?;796797		let function = match kind {798			FunctorKind::Function => self.clone(),799			FunctorKind::Functor => {800				let f = self801					.get_field("__functor")802					.context("getting functor value")?;803				assert_eq!(804					f.type_of(),805					NixType::Function,806					"invalid functor encountered"807				);808				f809			}810		};811812		let out = Value::new_uninit();813		with_default_context(|c, es| unsafe { value_call(c, es, function.0, v.0, out.0) })?;814815		Ok(out)816	}817	pub fn eval(v: &str) -> Result<Self> {818		let s = CString::new(v).expect("expression shouldn't have internal NULs");819		let out = Self::new_uninit();820		with_default_context(|c, es| unsafe {821			expr_eval_from_string(c, es, s.as_ptr(), c"/root".as_ptr(), out.0)822		})?;823		Ok(out)824	}825	pub fn build(&self, output: &str) -> Result<PathBuf> {826		if !self.is_derivation() {827			bail!("expected derivation to build")828		}829		let output_name = self830			.get_field("outputName")831			.context("getting output name field")?832			.to_string()?;833		let v = if output_name != output {834			let out = self.get_field(output).context("getting target output")?;835			if !out.is_derivation() {836				bail!("unknown output: {output}");837			}838			out839		} else {840			self.clone()841		};842		// to_string here blocks until the path is built843		let s = v.builtin_to_string()?;844		let rs = s.to_realised_string()?;845		let drv_path = rs.as_str().to_owned();846		Ok(PathBuf::from(drv_path))847	}848	pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {849		let to_json = Self::eval("builtins.toJSON")?;850		let s = to_json.call(self.clone())?.to_string()?;851		Ok(serde_json::from_str(&s)?)852	}853	pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {854		Self::eval(&nixlike::serialize(v)?)855	}856857	// Convert to string/evaluate derivations/etc858	// fn to_string_weak(&self) -> Result<String> {859	// 	// TODO: For now, it works exactly like to_string, see the comment for fn force()860	// 	self.to_string()861	// }862863	fn is_derivation(&self) -> bool {864		if !matches!(self.type_of(), NixType::Attrs) {865			return false;866		}867		let Some(ty) = self.get_field("type").ok() else {868			return false;869		};870		matches!(ty.to_string().as_deref(), Ok("derivation"))871	}872	fn functor_kind(&self) -> Option<FunctorKind> {873		match self.type_of() {874			NixType::Attrs => self875				.has_field("__functor")876				.expect("has_field shouldn't fail for attrs")877				.then_some(FunctorKind::Functor),878			NixType::Function => Some(FunctorKind::Function),879			_ => None,880		}881	}882	pub fn is_function(&self) -> bool {883		self.functor_kind().is_some()884	}885	pub fn is_null(&self) -> bool {886		matches!(self.type_of(), NixType::Null)887	}888	pub fn is_string(&self) -> bool {889		matches!(self.type_of(), NixType::String)890	}891	pub fn is_attrs(&self) -> bool {892		matches!(self.type_of(), NixType::Attrs)893	}894}895896impl From<String> for Value {897	fn from(value: String) -> Self {898		Value::new_str(&value)899	}900}901impl From<bool> for Value {902	fn from(value: bool) -> Self {903		Value::new_bool(value)904	}905}906impl From<&str> for Value {907	fn from(value: &str) -> Self {908		Value::new_str(value)909	}910}911impl<T> From<Vec<T>> for Value912where913	T: Into<Value>,914{915	fn from(value: Vec<T>) -> Self {916		Value::new_list(value)917	}918}919920impl Clone for Value {921	fn clone(&self) -> Self {922		with_default_context(|c, _| unsafe { value_incref(c, self.0) })923			.expect("value incref should not fail");924		Self(self.0)925	}926}927impl Drop for Value {928	fn drop(&mut self) {929		with_default_context(|c, _| unsafe { value_decref(c, self.0) })930			.expect("value drop should not fail");931	}932}933934pub fn init_libraries() {935	unsafe { GC_allow_register_threads() };936937	let mut ctx = NixContext::new();938	ctx.run_in_context(|c| unsafe { libutil_init(c) })939		.expect("util init should not fail");940	ctx.run_in_context(|c| unsafe { libstore_init(c) })941		.expect("store init should not fail");942	ctx.run_in_context(|c| unsafe { libexpr_init(c) })943		.expect("expr init should not fail");944945	nix_logging_cxx::apply_tracing_logger();946}947948unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(949	user_data: *mut c_void,950	context: *mut c_context,951	state: *mut nix_raw::EvalState,952	args: *mut *mut value,953	ret: *mut value,954) {955	let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };956	let mut e = None;957	let args: [&Value; N] = array::from_fn(|i| {958		let v: &mut Value = unsafe { &mut *args.add(i).cast() };959960		info!("forcing arg");961		if matches!(v.type_of(), NixType::Thunk)962			&& let Err(err) = v.force(state)963		{964			e = Some(err);965		};966		v as &Value967	});968	info!("args forced");969	let ctx: &mut NixContext = unsafe { &mut *context.cast() };970971	if let Some(e) = e {972		warn!("set err = {e}");973		unsafe { init_int(context, ret, 0) };974		return ctx.set_err(975			NixErrorKind::Unknown,976			&CString::new(e.to_string()).expect("forcing argument value failed"),977		);978	}979980	let state: &EvalState = unsafe { std::mem::transmute(&state) };981982	match user_closure(state, args) {983		Ok(v) => {984			unsafe { copy_value(context, ret, v.0) };985		}986		Err(e) => {987			unsafe { init_int(context, ret, 0) };988			warn!("set err = {e:#?}");989			ctx.set_err(990				NixErrorKind::Unknown,991				&CString::new(e.to_string()).expect("error should not contain internal nuls"),992			);993		}994	}995}996997type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;998999pub struct NativeFn(*mut PrimOp);1000impl NativeFn {1001	pub fn new<const N: usize>(1002		name: &'static CStr,1003		doc: &'static CStr,1004		args: [&'static CStr; N],1005		f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,1006	) -> Self {1007		// Double-boxing to make it thin pointer, as vtable gets outside of first Box1008		let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1009		let f: PrimOpFun = Some(nix_primop_closure_adapter::<N>);1010		let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec();1011		args.push(null());1012		let args = args.as_mut_ptr();1013		let primop = unsafe {1014			alloc_primop(1015				null_mut(),1016				f,1017				N as i32,1018				name.as_ptr(),1019				args,1020				doc.as_ptr(),1021				Box::into_raw(closure).cast(),1022			)1023		};10241025		assert!(!primop.is_null(), "primop allocation should not fail");10261027		Self(primop)1028	}1029	pub fn register(self) {1030		unsafe { register_primop(null_mut(), self.0) };1031	}1032}10331034struct StorePath(*mut c_store_path);1035impl StorePath {}10361037impl Drop for StorePath {1038	fn drop(&mut self) {1039		unsafe { store_path_free(self.0) }1040	}1041}10421043#[test_log::test]1044fn test_native() -> Result<()> {1045	init_libraries();1046	NativeFn::new(1047		c"__uppercaseSuffix2",1048		c"make string uppercase and add suffix",1049		[c"str", c"suffix"],1050		|_, [str, suffix]: [&Value; 2]| {1051			let str = str.to_string()?;1052			let suffix = suffix.to_string()?;1053			Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1054		},1055	)1056	.register();10571058	let mut fetch_settings = FetchSettings::new();1059	fetch_settings.set(c"warn-dirty", c"false");10601061	let manifest = format!("git+file://{}/../../", env!("CARGO_MANIFEST_DIR"));1062	let flake = FlakeSettings::new()?;1063	let parse = FlakeReferenceParseFlags::new(&flake)?;1064	let (mut r, _) = FlakeReference::new(&manifest, &flake, &parse, &fetch_settings)?;1065	let lock = FlakeLockFlags::new(&flake)?;1066	let locked = r.lock(&fetch_settings, &flake, &lock)?;1067	let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;10681069	let builtins = Value::eval("builtins")?;1070	assert_eq!(builtins.type_of(), NixType::Attrs);10711072	assert_eq!(attrs.type_of(), NixType::Attrs);1073	let test_data = nix_go!(attrs.testData);10741075	let test_string: String = nix_go_json!(test_data.testString);1076	assert_eq!(test_string, "hello");10771078	let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath);1079	let s = CString::new(s.to_string()?).expect("path str is cstring");10801081	let uppercase_suffix = Value::new_primop(NativeFn::new(1082		c"uppercase_suffix",1083		c"make string uppercase and add suffix",1084		[c"str", c"suffix"],1085		|es, [str, suffix]: [&Value; 2]| {1086			let str = str.to_string()?;1087			let suffix = suffix.to_string()?;1088			Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1089		},1090	));10911092	let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix));1093	assert_eq!(test_result, "PREFIX_BODY_SUFFIX");1094	let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));1095	assert_eq!(test_result, "TESTsuffix");10961097	let nix_ctx = NixContext::new();1098	let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?;10991100	// nix_raw::store_get_fs_closure(1);11011102	Ok(())1103}11041105// pub struct GcAlloc;1106// unsafe impl GlobalAlloc for GcAlloc {1107// 	unsafe fn alloc(&self, l: Layout) -> *mut u8 {1108// 		let ptr = unsafe { GC_malloc(l.size()) };1109// 		ptr.cast()1110// 	}1111// 	unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) {1112// 		// unsafe { GC_free(ptr.cast()) };1113// 	}1114//1115// 	unsafe fn realloc(&self, ptr: *mut u8, _: Layout, new_size: usize) -> *mut u8 {1116// 		let ptr = unsafe { GC_realloc(ptr.cast(), new_size) };1117// 		ptr.cast()1118// 	}1119// }1120//1121// #[global_allocator]1122// static GC: GcAlloc = GcAlloc;
after · crates/nix-eval/src/lib.rs
1use std::borrow::Cow;2use std::cell::RefCell;3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};4use std::ptr::{null, null_mut};5use std::sync::{Arc, LazyLock, OnceLock};6use std::{array, fmt, slice};7use std::{collections::HashMap, path::PathBuf};89use anyhow::{Context, anyhow, bail};10use itertools::Itertools;11use serde::Serialize;12use serde::de::DeserializeOwned;13use std::mem::transmute;1415pub use anyhow::Result;16use tracing::{Instrument, info, instrument, warn};1718use self::logging::{ErrorInfoBuilder, nix_logging_cxx};19use self::nix_cxx::set_fetcher_setting;20use self::nix_raw::{21	BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,22	GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base,23	GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,24	PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,25	bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,26	clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,27	err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,28	eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,29	expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,30	flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,31	flake_reference_and_fragment_from_string, flake_reference_parse_flags,32	flake_reference_parse_flags_free, flake_reference_parse_flags_new,33	flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,34	flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size,35	get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int,36	init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free,37	list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs,38	make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,39	realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,40	realised_string_get_store_path, realised_string_get_store_path_count, register_primop,41	set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,42	store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,43};4445// Contains macros helpers46pub mod logging;47#[doc(hidden)]48pub mod macros;49pub mod util;5051#[allow(52	non_upper_case_globals,53	non_camel_case_types,54	non_snake_case,55	dead_code56)]57mod nix_raw {58	include!(concat!(env!("OUT_DIR"), "/bindings.rs"));59}60#[cxx::bridge]61pub mod nix_cxx {62	unsafe extern "C++" {63		type nix_fetchers_settings;64		include!("nix-eval/src/lib.hh");6566		#[allow(clippy::missing_safety_doc)]67		unsafe fn set_fetcher_setting(68			settings: *mut nix_fetchers_settings,69			setting: *const c_char,70			value: *const c_char,71		);72	}73}7475#[derive(Debug, PartialEq, Eq)]76pub enum NixType {77	Thunk,78	Int,79	Float,80	Bool,81	String,82	Path,83	Null,84	Attrs,85	List,86	Function,87	External,88}89impl NixType {90	fn from_int(c: c_uint) -> Self {91		match c {92			0 => Self::Thunk,93			1 => Self::Int,94			2 => Self::Float,95			3 => Self::Bool,96			4 => Self::String,97			5 => Self::Path,98			6 => Self::Null,99			7 => Self::Attrs,100			8 => Self::List,101			9 => Self::Function,102			10 => Self::External,103			_ => unreachable!("unknown nix type: {c}"),104		}105	}106}107108enum FunctorKind {109	Function,110	Functor,111}112113#[derive(Debug)]114#[repr(i32)]115pub enum NixErrorKind {116	Unknown = err_NIX_ERR_UNKNOWN,117	Overflow = err_NIX_ERR_OVERFLOW,118	Key = err_NIX_ERR_KEY,119	Generic = err_NIX_ERR_NIX_ERROR,120}121impl NixErrorKind {122	fn from_int(v: c_int) -> Option<Self> {123		Some(match v {124			0 => return None,125			nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,126			nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,127			nix_raw::err_NIX_ERR_KEY => Self::Key,128			nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,129			_ => {130				debug_assert!(false, "unexpected nix error kind: {v}");131				Self::Unknown132			}133		})134	}135}136137pub fn gc_now() {138	unsafe { gc_now_raw() };139}140141pub fn gc_register_my_thread() {142	assert_eq!(unsafe { GC_thread_is_registered() }, 0);143144	let mut sb = GC_stack_base {145		mem_base: null_mut(),146	};147	let r = unsafe { GC_get_stack_base(&mut sb) };148	if r as u32 != GC_SUCCESS {149		panic!("failed to get thread stack base");150	}151	unsafe { GC_register_my_thread(&sb) };152}153pub fn gc_unregister_my_thread() {154	assert_eq!(unsafe { GC_thread_is_registered() }, 1);155156	unsafe { GC_unregister_my_thread() };157}158159pub struct ThreadRegisterGuard {}160impl ThreadRegisterGuard {161	#[allow(clippy::new_without_default)]162	pub fn new() -> Self {163		gc_register_my_thread();164		Self {}165	}166}167impl Drop for ThreadRegisterGuard {168	fn drop(&mut self) {169		gc_unregister_my_thread();170	}171}172173#[repr(transparent)]174pub struct NixContext(*mut c_context);175impl NixContext {176	pub fn set_err_raw(&mut self, err: NixErrorKind, msg: &CStr) {177		unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };178	}179	pub fn set_err(&mut self, err: anyhow::Error) {180		let mut fmt = format!("{err:?}").replace("\0", "\\0");181		self.set_err_raw(182			NixErrorKind::Generic,183			&CString::new(fmt).expect("NUL bytes were just replaced"),184		);185	}186	pub fn new() -> Self {187		let ctx = unsafe { c_context_create() };188		Self(ctx)189	}190	fn error_kind(&self) -> Option<NixErrorKind> {191		let code = unsafe { err_code(self.0) };192		NixErrorKind::from_int(code)193	}194	fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {195		if let NixErrorKind::Generic = self.error_kind()? {196			let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };197			let mut err_out = String::new();198			unsafe {199				err_info_msg(200					null_mut(),201					self.0,202					Some(copy_nix_str),203					(&raw mut err_out).cast(),204				)205			};206			return Some((Cow::Owned(err_out), Some(ei)));207		};208209		// TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,210		// but it looks ugly211		let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };212		Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))213	}214	fn clean_err(&mut self) {215		unsafe {216			clear_err(self.0);217		}218	}219220	fn bail_if_error(&self) -> Result<()> {221		if let Some((err, stack)) = self.error() {222			let mut e = Err(anyhow!("{err}"));223			if let Some(stack) = stack {224				for ele in stack.stack_frames {225					e = e.with_context(|| {226						if ele.pos.is_empty() {227							ele.msg228						} else {229							format!("{} at {}", ele.msg, ele.pos)230						}231					})232				}233			}234			return e.context("<nix frames>");235		};236		Ok(())237	}238239	fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {240		self.clean_err();241		let o = f(self.0);242		self.bail_if_error()?;243		self.clean_err();244		Ok(o)245	}246}247248impl Default for NixContext {249	fn default() -> Self {250		Self::new()251	}252}253impl Drop for NixContext {254	fn drop(&mut self) {255		unsafe {256			c_context_free(self.0);257		}258	}259}260struct GlobalState {261	// Store should be valid as long as EvalState is valid262	#[allow(dead_code)]263	store: Store,264	state: EvalState,265}266impl GlobalState {267	fn new() -> Result<Self> {268		let mut ctx = NixContext::new();269		let store = ctx270			.run_in_context(|c| unsafe { store_open(c, c"auto".as_ptr(), null_mut()) })271			.map(Store)?;272273		let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;274		ctx.run_in_context(|c| unsafe { eval_state_builder_load(c, builder) })?;275		ctx.run_in_context(|c| unsafe {276			eval_state_builder_set_eval_setting(277				c,278				builder,279				c"lazy-trees".as_ptr(),280				c"true".as_ptr(),281			)282		})?;283		ctx.run_in_context(|c| unsafe {284			eval_state_builder_set_eval_setting(285				c,286				builder,287				c"lazy-locks".as_ptr(),288				c"true".as_ptr(),289			)290		})?;291		let state = ctx292			.run_in_context(|c| unsafe { eval_state_build(c, builder) })293			.map(EvalState)?;294295		Ok(Self { store, state })296	}297}298299struct ThreadState {300	ctx: NixContext,301}302impl ThreadState {303	fn new() -> Result<Self> {304		let ctx = NixContext::new();305306		Ok(Self { ctx })307	}308}309310static GLOBAL_STATE: LazyLock<GlobalState> = LazyLock::new(|| {311	info!("initializing nix global state");312	GlobalState::new().expect("global state init shouldn't fail")313});314315thread_local! {316	static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));317}318fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {319	let global = &GLOBAL_STATE.state;320	let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));321	let mut ctx = NixContext(ctx);322	let v = ctx.run_in_context(|c| f(c, state));323	// It is reused for thread324	std::mem::forget(ctx);325	v326}327328pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> {329	with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())330}331332pub struct FetchSettings(*mut fetchers_settings);333impl FetchSettings {334	pub fn new() -> Self {335		Self::try_new().expect("allocation should not fail")336	}337	fn try_new() -> Result<Self> {338		with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)339	}340	pub fn set(&mut self, setting: &CStr, value: &CStr) {341		unsafe {342			set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());343		};344	}345}346unsafe impl Send for FetchSettings {}347unsafe impl Sync for FetchSettings {}348349impl Default for FetchSettings {350	fn default() -> Self {351		Self::new()352	}353}354355impl Drop for FetchSettings {356	fn drop(&mut self) {357		unsafe { fetchers_settings_free(self.0) };358	}359}360pub struct FlakeSettings(*mut flake_settings);361impl FlakeSettings {362	pub fn new() -> Result<Self> {363		with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)364	}365}366unsafe impl Send for FlakeSettings {}367unsafe impl Sync for FlakeSettings {}368impl Drop for FlakeSettings {369	fn drop(&mut self) {370		unsafe {371			flake_settings_free(self.0);372		}373	}374}375376pub struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);377impl FlakeReferenceParseFlags {378	pub fn new(settings: &FlakeSettings) -> Result<Self> {379		with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })380			.map(Self)381	}382	pub fn set_base_dir(&mut self, dir: &str) -> Result<()> {383		with_default_context(|c, _| {384			unsafe {385				flake_reference_parse_flags_set_base_directory(386					c,387					self.0,388					dir.as_ptr().cast(),389					dir.len(),390				)391			};392		})393	}394}395impl Drop for FlakeReferenceParseFlags {396	fn drop(&mut self) {397		unsafe {398			flake_reference_parse_flags_free(self.0);399		}400	}401}402pub struct FlakeLockFlags(*mut flake_lock_flags);403impl FlakeLockFlags {404	pub fn new(settings: &FlakeSettings) -> Result<Self> {405		let o = with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) })406			.map(Self)?;407		// with_default_context(|c, _| unsafe { flake_lock_flags_set_mode_virtual(c, o.0) })?;408409		Ok(o)410	}411}412impl Drop for FlakeLockFlags {413	fn drop(&mut self) {414		unsafe {415			flake_lock_flags_free(self.0);416		}417	}418}419420unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {421	let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };422	let s = std::str::from_utf8(s).expect("c string has invalid utf-8");423	unsafe { *user_data.cast::<String>() = s.to_owned() };424}425426struct Store(*mut c_store);427unsafe impl Send for Store {}428unsafe impl Sync for Store {}429430impl Store {431	fn parse_path(&self, path: &CStr) -> Result<StorePath> {432		with_default_context(|c, _| {433			StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })434		})435	}436}437438#[repr(transparent)]439pub struct EvalState(*mut c_eval_state);440unsafe impl Send for EvalState {}441unsafe impl Sync for EvalState {}442443impl Drop for EvalState {444	fn drop(&mut self) {445		unsafe {446			state_free(self.0);447		}448	}449}450451pub struct FlakeReference(*mut flake_reference);452impl FlakeReference {453	#[instrument(name = "new-flake-reference", skip(flake, parse, fetch))]454	pub fn new(455		s: &str,456		flake: &FlakeSettings,457		parse: &FlakeReferenceParseFlags,458		fetch: &FetchSettings,459	) -> Result<(Self, String)> {460		let mut out = null_mut();461		let mut fragment = String::new();462		// let fetch_settings = fetcher_settings;463		with_default_context(|c, _| unsafe {464			flake_reference_and_fragment_from_string(465				c,466				fetch.0,467				flake.0,468				parse.0,469				s.as_ptr().cast(),470				s.len(),471				&mut out,472				Some(copy_nix_str),473				(&raw mut fragment).cast(),474			)475		})?;476		assert!(!out.is_null());477478		Ok((Self(out), fragment))479	}480	#[instrument(name = "lock-flake", skip(self, fetch, flake, lock))]481	pub fn lock(482		&mut self,483		fetch: &FetchSettings,484		flake: &FlakeSettings,485		lock: &FlakeLockFlags,486	) -> Result<LockedFlake> {487		with_default_context(|c, es| unsafe { flake_lock(c, fetch.0, flake.0, es, lock.0, self.0) })488			.map(LockedFlake)489	}490}491unsafe impl Send for FlakeReference {}492unsafe impl Sync for FlakeReference {}493494pub struct LockedFlake(*mut locked_flake);495impl LockedFlake {496	pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {497		with_default_context(|c, es| unsafe {498			locked_flake_get_output_attrs(c, settings.0, es, self.0)499		})500		.map(Value)501	}502}503unsafe impl Send for LockedFlake {}504unsafe impl Sync for LockedFlake {}505impl Drop for LockedFlake {506	fn drop(&mut self) {507		unsafe {508			locked_flake_free(self.0);509		};510	}511}512513type FieldName = [u8; 64];514fn init_field_name(v: &str) -> FieldName {515	let mut f = [0; 64];516	assert!(v.len() < 64, "max field name is 63 chars");517	assert!(518		v.bytes().all(|v| v != 0),519		"nul bytes are unsupported in field name"520	);521	f[0..v.len()].copy_from_slice(v.as_bytes());522	f523}524525pub struct RealisedString(*mut realised_string);526impl fmt::Debug for RealisedString {527	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {528		self.as_str().fmt(f)529	}530}531532impl RealisedString {533	pub fn as_str(&self) -> &str {534		let len = unsafe { realised_string_get_buffer_size(self.0) };535		let data: *const u8 = unsafe { realised_string_get_buffer_start(self.0) }.cast();536		let data = unsafe { slice::from_raw_parts(data, len) };537		std::str::from_utf8(data).expect("non-utf8 strings not supported")538	}539	pub fn path_count(&self) -> usize {540		unsafe { realised_string_get_store_path_count(self.0) }541	}542	pub fn path(&self, i: usize) -> String {543		assert!(i < self.path_count());544		let path = unsafe { realised_string_get_store_path(self.0, i) };545		let mut err_out = String::new();546		unsafe { store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };547		err_out548	}549}550551unsafe impl Send for RealisedString {}552impl Drop for RealisedString {553	fn drop(&mut self) {554		unsafe { realised_string_free(self.0) }555	}556}557558#[repr(transparent)]559pub struct Value(*mut value);560561unsafe impl Send for Value {}562unsafe impl Sync for Value {}563564pub trait AsFieldName {565	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;566	fn to_field_name(&self) -> Result<String>;567}568impl AsFieldName for Value {569	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {570		let f = self.to_string()?;571		v(init_field_name(&f))572	}573	fn to_field_name(&self) -> Result<String> {574		self.to_string()575	}576}577impl<E> AsFieldName for E578where579	E: AsRef<str>,580{581	fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {582		let f = self.as_ref();583		v(init_field_name(f))584	}585	fn to_field_name(&self) -> Result<String> {586		Ok(self.as_ref().to_owned())587	}588}589590struct AttrsBuilder(*mut c_bindings_builder);591impl AttrsBuilder {592	fn new(capacity: usize) -> Self {593		with_default_context(|c, es| unsafe { make_bindings_builder(c, es, capacity) })594			.map(Self)595			.expect("alloc should not fail")596	}597	fn insert(&mut self, k: &impl AsFieldName, v: Value) {598		k.as_field_name(|name| {599			with_default_context(|c, _| unsafe {600				bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0);601				// bindings_builder_insert doesn't do incref602			})603		})604		.expect("builder insert shouldn't fail");605	}606}607impl Drop for AttrsBuilder {608	fn drop(&mut self) {609		unsafe { bindings_builder_free(self.0) };610	}611}612613struct ListBuilder(*mut c_list_builder, c_uint);614impl ListBuilder {615	fn new(capacity: usize) -> Self {616		with_default_context(|c, es| unsafe { make_list_builder(c, es, capacity) })617			.map(|l| Self(l, 0))618			.expect("alloc should not fail")619	}620}621impl ListBuilder {622	fn push(&mut self, v: Value) {623		with_default_context(|c, _| unsafe {624			list_builder_insert(625				c,626				self.0,627				{628					let v = self.1;629					self.1 += 1;630					v631				},632				v.0,633			)634		})635		.expect("list insert shouldn't fail");636	}637}638impl Drop for ListBuilder {639	fn drop(&mut self) {640		unsafe { list_builder_free(self.0) };641	}642}643644impl Value {645	pub fn new_primop(v: NativeFn) -> Self {646		let out = Self::new_uninit();647		with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) })648			.expect("primop initialization should not fail");649		out650	}651	pub fn new_attrs(v: HashMap<&str, Value>) -> Self {652		let out = Self::new_uninit();653		let mut b = AttrsBuilder::new(v.len());654		for (k, v) in v {655			b.insert(&k, v);656		}657		with_default_context(|c, _| unsafe { make_attrs(c, out.0, b.0) })658			.expect("attrs initialization should not fail");659660		out661	}662	fn new_list<T: Into<Self>>(v: Vec<T>) -> Self {663		let out = Self::new_uninit();664		let mut b = ListBuilder::new(v.len());665		for v in v {666			b.push(v.into());667		}668		with_default_context(|c, _| unsafe { make_list(c, b.0, out.0) })669			.expect("list initialization should not fail");670671		out672	}673	fn new_uninit() -> Self {674		let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })675			.expect("value allocation should not fail");676		Self(out)677	}678	pub fn new_str(v: &str) -> Self {679		let s = CString::new(v).expect("string should not contain NULs");680		let out = Self::new_uninit();681		// String is copied, `s` is free to be dropped682		with_default_context(|c, _| unsafe { init_string(c, out.0, s.as_ptr()) })683			.expect("string initialization should not fail");684		out685	}686	pub fn new_int(i: i64) -> Self {687		let out = Self::new_uninit();688		with_default_context(|c, _| unsafe { init_int(c, out.0, i) })689			.expect("int initialization should not fail");690		out691	}692	pub fn new_bool(v: bool) -> Self {693		let out = Self::new_uninit();694		with_default_context(|c, _| unsafe { init_bool(c, out.0, v) })695			.expect("bool initialization should not fail");696		out697	}698	// TODO: As far as I can see, there is no way to get Thunks from nix public C api, so this function is useless699	// fn force(&mut self, st: &mut EvalState) -> Result<()> {700	// 	with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;701	// 	Ok(())702	// }703	pub fn type_of(&self) -> NixType {704		let ty = with_default_context(|c, _| unsafe { get_type(c, self.0) })705			.expect("get_type should not fail");706		NixType::from_int(ty)707	}708	fn builtin_to_string(&self) -> Result<Self> {709		let builtin = Self::eval("builtins.toString")?;710		builtin.call(self.clone())711	}712	fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {713		with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;714		Ok(())715	}716	pub fn to_string(&self) -> Result<String> {717		let ty = self.type_of();718		if !matches!(ty, NixType::String) {719			bail!("unexpected type: {ty:?}, expected string");720		}721		let mut str_out = String::new();722		with_default_context(|c, _| unsafe {723			get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())724		})?;725726		Ok(str_out)727	}728	pub fn to_realised_string(&self) -> Result<RealisedString> {729		with_default_context(|c, es| unsafe { string_realise(c, es, self.0, false) })730			.map(RealisedString)731732		// let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };733		// for i in 0..store_paths {734		// 	let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };735		// 	nix_raw::store_path_name(store_path, callback, user_data);736		// }737		// dbg!(store_paths);738		// todo!();739	}740741	pub fn has_field(&self, field: &str) -> Result<bool> {742		if !matches!(self.type_of(), NixType::Attrs) {743			bail!("invalid type: expected attrs");744		}745746		let f = init_field_name(field);747		with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })748	}749	// pub fn derivation_path(&self) {750	// 	nix_raw::real751	// }752	pub fn list_fields(&self) -> Result<Vec<String>> {753		if !matches!(self.type_of(), NixType::Attrs) {754			bail!("invalid type: expected attrs");755		}756757		let len = with_default_context(|c, _| unsafe { get_attrs_size(c, self.0) })?;758		let mut out = Vec::with_capacity(len as usize);759760		for i in 0..len {761			let name =762				with_default_context(|c, es| unsafe { get_attr_name_byidx(c, self.0, es, i) })?;763			let c = unsafe { CStr::from_ptr(name) };764			out.push(c.to_str().expect("nix field names are utf-8").to_owned());765		}766		Ok(out)767	}768	pub fn get_elem(&self, v: usize) -> Result<Self> {769		if !matches!(self.type_of(), NixType::List) {770			bail!("invalid type: expected list");771		}772		let len = with_default_context(|c, _| unsafe { get_list_size(c, self.0) })? as usize;773		if v >= len {774			bail!("oob list get: {v} >= {len}");775		}776777		with_default_context(|c, es| unsafe { get_list_byidx(c, self.0, es, v as u32) }).map(Self)778	}779	pub fn attrs_update(self, other: Value /*, ignore_errors: bool*/) -> Result<Self> {780		let attrs_update_fn = Self::eval("a: b: a // b")?;781782		attrs_update_fn783			.call(self)?784			.call(other)785			.context("attrs update")786	}787	pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {788		if !matches!(self.type_of(), NixType::Attrs) {789			bail!("invalid type: expected attrs");790		}791792		name.as_field_name(|name| {793			with_default_context(|c, es| unsafe {794				get_attr_byname(c, self.0, es, name.as_ptr().cast())795			})796			.map(Self)797		})798		.with_context(|| format!("getting field {:?}", name.to_field_name()))799	}800	pub fn call(&self, v: Value) -> Result<Self> {801		let kind = self802			.functor_kind()803			.ok_or_else(|| anyhow!("can only call function or functor"))?;804805		let function = match kind {806			FunctorKind::Function => self.clone(),807			FunctorKind::Functor => {808				let f = self809					.get_field("__functor")810					.context("getting functor value")?;811				assert_eq!(812					f.type_of(),813					NixType::Function,814					"invalid functor encountered"815				);816				f817			}818		};819820		let out = Value::new_uninit();821		with_default_context(|c, es| unsafe { value_call(c, es, function.0, v.0, out.0) })?;822823		Ok(out)824	}825	pub fn eval(v: &str) -> Result<Self> {826		let s = CString::new(v).expect("expression shouldn't have internal NULs");827		let out = Self::new_uninit();828		with_default_context(|c, es| unsafe {829			expr_eval_from_string(c, es, s.as_ptr(), c"/root".as_ptr(), out.0)830		})?;831		Ok(out)832	}833	pub fn build(&self, output: &str) -> Result<PathBuf> {834		if !self.is_derivation() {835			bail!("expected derivation to build")836		}837		let output_name = self838			.get_field("outputName")839			.context("getting output name field")?840			.to_string()?;841		let v = if output_name != output {842			let out = self.get_field(output).context("getting target output")?;843			if !out.is_derivation() {844				bail!("unknown output: {output}");845			}846			out847		} else {848			self.clone()849		};850		// to_string here blocks until the path is built851		let s = v.builtin_to_string()?;852		let rs = s.to_realised_string()?;853		let drv_path = rs.as_str().to_owned();854		Ok(PathBuf::from(drv_path))855	}856	pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {857		let to_json = Self::eval("builtins.toJSON")?;858		let s = to_json.call(self.clone())?.to_string()?;859		Ok(serde_json::from_str(&s)?)860	}861	pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {862		Self::eval(&nixlike::serialize(v)?)863	}864865	// Convert to string/evaluate derivations/etc866	// fn to_string_weak(&self) -> Result<String> {867	// 	// TODO: For now, it works exactly like to_string, see the comment for fn force()868	// 	self.to_string()869	// }870871	fn is_derivation(&self) -> bool {872		if !matches!(self.type_of(), NixType::Attrs) {873			return false;874		}875		let Some(ty) = self.get_field("type").ok() else {876			return false;877		};878		matches!(ty.to_string().as_deref(), Ok("derivation"))879	}880	fn functor_kind(&self) -> Option<FunctorKind> {881		match self.type_of() {882			NixType::Attrs => self883				.has_field("__functor")884				.expect("has_field shouldn't fail for attrs")885				.then_some(FunctorKind::Functor),886			NixType::Function => Some(FunctorKind::Function),887			_ => None,888		}889	}890	pub fn is_function(&self) -> bool {891		self.functor_kind().is_some()892	}893	pub fn is_null(&self) -> bool {894		matches!(self.type_of(), NixType::Null)895	}896	pub fn is_string(&self) -> bool {897		matches!(self.type_of(), NixType::String)898	}899	pub fn is_attrs(&self) -> bool {900		matches!(self.type_of(), NixType::Attrs)901	}902}903904impl From<String> for Value {905	fn from(value: String) -> Self {906		Value::new_str(&value)907	}908}909impl From<bool> for Value {910	fn from(value: bool) -> Self {911		Value::new_bool(value)912	}913}914impl From<&str> for Value {915	fn from(value: &str) -> Self {916		Value::new_str(value)917	}918}919impl<T> From<Vec<T>> for Value920where921	T: Into<Value>,922{923	fn from(value: Vec<T>) -> Self {924		Value::new_list(value)925	}926}927928impl Clone for Value {929	fn clone(&self) -> Self {930		with_default_context(|c, _| unsafe { value_incref(c, self.0) })931			.expect("value incref should not fail");932		Self(self.0)933	}934}935impl Drop for Value {936	fn drop(&mut self) {937		with_default_context(|c, _| unsafe { value_decref(c, self.0) })938			.expect("value drop should not fail");939	}940}941942static TOKIO_FOR_NIX: OnceLock<Arc<tokio::runtime::Runtime>> = OnceLock::new();943944pub fn init_libraries() {945	unsafe { GC_allow_register_threads() };946947	let mut ctx = NixContext::new();948	ctx.run_in_context(|c| unsafe { libutil_init(c) })949		.expect("util init should not fail");950	ctx.run_in_context(|c| unsafe { libstore_init(c) })951		.expect("store init should not fail");952	ctx.run_in_context(|c| unsafe { libexpr_init(c) })953		.expect("expr init should not fail");954955	nix_logging_cxx::apply_tracing_logger();956}957958pub fn init_tokio_for_nix(tokio: Arc<tokio::runtime::Runtime>) {959	TOKIO_FOR_NIX960		.set(tokio)961		.expect("tokio for nix should only be initialized once");962}963964pub fn await_in_nix<F: Send + 'static>(f: impl Future<Output = F> + Send + 'static) -> F {965	// It should be possible to do Handle::current(), but some of the planned features don't work well with that966	let runtime = TOKIO_FOR_NIX967		.get()968		.expect("init_tokio_for_nix was not called");969	std::thread::spawn(move || runtime.block_on(f)).join().expect("await_in_nix inner thread panicked")970}971972unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(973	user_data: *mut c_void,974	mut context: *mut c_context,975	state: *mut nix_raw::EvalState,976	args: *mut *mut value,977	ret: *mut value,978) {979	let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };980	let args: [&Value; N] = array::from_fn(|i| {981		let v: &mut Value = unsafe { &mut *args.add(i).cast() };982		v as &Value983	});984	let ctx: &mut NixContext = unsafe { transmute(&mut context) };985986	let state: &EvalState = unsafe { std::mem::transmute(&state) };987988	match user_closure(state, args) {989		Ok(v) => {990			unsafe { copy_value(context, ret, v.0) };991		}992		Err(e) => {993			ctx.set_err(e);994		}995	}996}997998type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;9991000pub struct NativeFn(*mut PrimOp);1001impl NativeFn {1002	pub fn new<const N: usize>(1003		name: &'static CStr,1004		doc: &'static CStr,1005		args: [&'static CStr; N],1006		f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,1007	) -> Self {1008		// Double-boxing to make it thin pointer, as vtable gets outside of first Box1009		let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1010		let f: PrimOpFun = Some(nix_primop_closure_adapter::<N>);1011		let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec();1012		args.push(null());1013		let args = args.as_mut_ptr();1014		let primop = unsafe {1015			alloc_primop(1016				null_mut(),1017				f,1018				N as i32,1019				name.as_ptr(),1020				args,1021				doc.as_ptr(),1022				Box::into_raw(closure).cast(),1023			)1024		};10251026		assert!(!primop.is_null(), "primop allocation should not fail");10271028		Self(primop)1029	}1030	pub fn register(self) {1031		unsafe { register_primop(null_mut(), self.0) };1032	}1033}10341035struct StorePath(*mut c_store_path);1036impl StorePath {}10371038impl Drop for StorePath {1039	fn drop(&mut self) {1040		unsafe { store_path_free(self.0) }1041	}1042}10431044#[test_log::test]1045fn test_native() -> Result<()> {1046	init_libraries();1047	NativeFn::new(1048		c"__uppercaseSuffix2",1049		c"make string uppercase and add suffix",1050		[c"str", c"suffix"],1051		|_, [str, suffix]: [&Value; 2]| {1052			let str = str.to_string()?;1053			let suffix = suffix.to_string()?;1054			Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1055		},1056	)1057	.register();10581059	let mut fetch_settings = FetchSettings::new();1060	fetch_settings.set(c"warn-dirty", c"false");10611062	let manifest = format!("git+file://{}/../../", env!("CARGO_MANIFEST_DIR"));1063	let flake = FlakeSettings::new()?;1064	let parse = FlakeReferenceParseFlags::new(&flake)?;1065	let (mut r, _) = FlakeReference::new(&manifest, &flake, &parse, &fetch_settings)?;1066	let lock = FlakeLockFlags::new(&flake)?;1067	let locked = r.lock(&fetch_settings, &flake, &lock)?;1068	let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;10691070	let builtins = Value::eval("builtins")?;1071	assert_eq!(builtins.type_of(), NixType::Attrs);10721073	assert_eq!(attrs.type_of(), NixType::Attrs);1074	let test_data = nix_go!(attrs.testData);10751076	let test_string: String = nix_go_json!(test_data.testString);1077	assert_eq!(test_string, "hello");10781079	let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath);1080	let s = CString::new(s.to_string()?).expect("path str is cstring");10811082	let uppercase_suffix = Value::new_primop(NativeFn::new(1083		c"uppercase_suffix",1084		c"make string uppercase and add suffix",1085		[c"str", c"suffix"],1086		|es, [str, suffix]: [&Value; 2]| {1087			let str = str.to_string()?;1088			let suffix = suffix.to_string()?;1089			Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1090		},1091	));10921093	let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix));1094	assert_eq!(test_result, "PREFIX_BODY_SUFFIX");1095	let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));1096	assert_eq!(test_result, "TESTsuffix");10971098	let nix_ctx = NixContext::new();1099	let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?;11001101	// nix_raw::store_get_fs_closure(1);11021103	Ok(())1104}11051106// pub struct GcAlloc;1107// unsafe impl GlobalAlloc for GcAlloc {1108// 	unsafe fn alloc(&self, l: Layout) -> *mut u8 {1109// 		let ptr = unsafe { GC_malloc(l.size()) };1110// 		ptr.cast()1111// 	}1112// 	unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) {1113// 		// unsafe { GC_free(ptr.cast()) };1114// 	}1115//1116// 	unsafe fn realloc(&self, ptr: *mut u8, _: Layout, new_size: usize) -> *mut u8 {1117// 		let ptr = unsafe { GC_realloc(ptr.cast(), new_size) };1118// 		ptr.cast()1119// 	}1120// }1121//1122// #[global_allocator]1123// static GC: GcAlloc = GcAlloc;
modifiedcrates/nix-eval/src/macros.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/macros.rs
+++ b/crates/nix-eval/src/macros.rs
@@ -68,13 +68,13 @@
 #[macro_export]
 macro_rules! nix_go {
 	(@o($o:expr, $path:expr) . $var:ident $($tt:tt)*) => {{
-		nix_go!(@o($o.get_field(stringify!($var)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+		nix_go!(@o(tokio::task::block_in_place(|| $o.get_field(stringify!($var))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
 	}};
 	(@o($o:expr, $path:expr) [ $v:expr ] $($tt:tt)*) => {{
-		nix_go!(@o($o.get_field($v).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+		nix_go!(@o(tokio::task::block_in_place(|| $o.get_field($v)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
 	}};
 	(@o($o:expr, $path:expr) ($($var:tt)*) $($tt:tt)*) => {
-		nix_go!(@o($o.call($crate::nix_expr_inner!($($var)+)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+		nix_go!(@o(tokio::task::block_in_place(|| $o.call($crate::nix_expr_inner!($($var)+))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
 	};
 	(@o($o:expr, $path:expr)) => {$o};
 	($field:ident $($tt:tt)+) => {{
@@ -87,6 +87,6 @@
 #[macro_export]
 macro_rules! nix_go_json {
 	($($tt:tt)*) => {{
-		$crate::nix_go!($($tt)*).as_json()?
+		tokio::task::block_in_place(|| $crate::nix_go!($($tt)*).as_json())?
 	}};
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -2,10 +2,10 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1767461147,
+        "lastModified": 1768700043,
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "7d59256814085fd9666a2ae3e774dc5ee216b630",
+        "rev": "935de8bd6838d940988bb065be2a2034259327b9",
         "type": "github"
       },
       "original": {
@@ -37,10 +37,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1767609335,
+        "lastModified": 1768135262,
         "owner": "hercules-ci",
         "repo": "flake-parts",
-        "rev": "250481aafeb741edfe23d29195671c19b36b6dca",
+        "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
         "type": "github"
       },
       "original": {
@@ -111,10 +111,10 @@
         "nixpkgs-regression": "nixpkgs-regression"
       },
       "locked": {
-        "lastModified": 1767670640,
+        "lastModified": 1768702010,
         "owner": "deltarocks",
         "repo": "nix",
-        "rev": "2181cd07134c9049bd77b7f48c3b1ea8647267de",
+        "rev": "b05b52670b9c7affff5b9be3edb539a1603c39e6",
         "type": "github"
       },
       "original": {
@@ -126,10 +126,10 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1767657734,
+        "lastModified": 1768697925,
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "d4ccebf51ee4dbeb9df364dce1fe9848635c1258",
+        "rev": "665062f7df2c7db8fdbbec4f1b730091143828a3",
         "type": "github"
       },
       "original": {
@@ -190,10 +190,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1767667566,
+        "lastModified": 1768617670,
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e",
+        "rev": "56d0fbdd732f3686e8414b857cf885038fc17d57",
         "type": "github"
       },
       "original": {
@@ -223,10 +223,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1767468822,
+        "lastModified": 1768158989,
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "d56486eb9493ad9c4777c65932618e9c2d0468fc",
+        "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
         "type": "github"
       },
       "original": {
modifiedmodules/nixos.nixdiffbeforeafterboth
--- a/modules/nixos.nix
+++ b/modules/nixos.nix
@@ -10,7 +10,7 @@
 let
   inherit (lib.attrsets) mapAttrs;
   inherit (lib.options) mkOption;
-  inherit (lib.types) deferredModule unspecified;
+  inherit (lib.types) deferredModule unspecified uniq str;
   inherit (lib.strings) escapeNixIdentifier;
   inherit (fleetLib.options) mkHostsOption;
 
@@ -24,9 +24,18 @@
       '';
       type = deferredModule;
     };
-    hosts = mkHostsOption (hostArgs: {
+    hosts = mkHostsOption (hostArgs: let
+      hostName = hostArgs.config._module.args.name;
+    in {
       inherit _file;
       options = {
+        name = mkOption {
+          description = ''
+            Host name (alias)
+          '';
+          type = uniq str;
+          default = hostName;
+        };
         nixos = mkOption {
           description = ''
             Nixos configuration for the current host.
@@ -42,7 +51,7 @@
               prefix = [
                 "fleetConfiguration"
                 "hosts"
-                hostArgs.config._module.args.name
+                hostName
                 "nixos"
               ];
               modules = (import "${modulesPath}/module-list.nix") ++ [
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -15,7 +15,12 @@
   inherit (lib.stringsWithDeps) stringAfter;
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.lists) optional elem;
-  inherit (lib.attrsets) mapAttrs mapAttrsToList;
+  inherit (lib.attrsets)
+    mapAttrs
+    mapAttrsToList
+    filterAttrs
+    attrNames
+    ;
   inherit (lib.modules) mkIf;
   inherit (lib.types)
     submodule
@@ -25,9 +30,9 @@
     uniq
     functionTo
     package
-    bool
     enum
     either
+    listOf
     ;
   inherit (fleetLib.strings) decodeRawSecret;
 
@@ -132,10 +137,33 @@
 in
 {
   options = {
+    _providedSharedSecrets = mkOption {
+      description = ''
+        List of shared secrets, for which the current host was specified as `expectedOwners`
+      '';
+      type = listOf str;
+      default = [];
+      internal = true;
+    };
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
-      apply = mapAttrs (_: secret: secret.parts // { definition = secret; });
+      apply =
+        secrets:
+        mapAttrs (_: secret: secret.parts // { definition = secret; })
+
+          (
+            let
+              hostName = host.name;
+              expectedNonshared = attrNames (filterAttrs (_: def: def.generator != "shared") secrets);
+              expectedShared = config._providedSharedSecrets;
+            in
+            builtins.deepSeq [
+              hostName
+              expectedNonshared
+              expectedShared
+            ] (builtins.fleetEnsureHostSecrets hostName expectedNonshared expectedShared secrets)
+          );
       description = "Host-local secrets";
     };
     system.secretsData = mkOption {
@@ -163,7 +191,7 @@
           (secret.definition.generator == "shared") == hasSharedDefinition
           && (
             hasSharedDefinition
-            -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners)
+            -> (elem host.name fleetConfiguration.secrets.${name}.expectedOwners)
           );
         message =
           if hasSharedDefinition then
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -1,9 +1,10 @@
 {
   lib,
+  config,
   ...
 }:
 let
-  inherit (lib.options) mkOption literalExpression;
+  inherit (lib.options) mkOption;
   inherit (lib.types)
     nullOr
     listOf
@@ -16,6 +17,8 @@
     uniq
     ;
   inherit (lib.strings) concatStringsSep;
+  inherit (lib.lists) elem filter;
+  inherit (lib.attrsets) attrNames;
 
   sharedSecret =
     { config, ... }:
@@ -30,28 +33,33 @@
         regenerateOnOwnerAdded = mkOption {
           type = bool;
           description = ''
-            Controls whether the secret must be regenerated when new owners are added.
+            Whether the secret prefers to be rotated when new owners are added.
 
-            Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).
-            When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.
+            Note that this is only a security measure, if the secret needs to be regenerated due to e.g X.509 SANs
+            changes - then you most likely want to use generationData for that instead.
           '';
+          default = false;
         };
         regenerateOnOwnerRemoved = mkOption {
-          default = config.regenerateOnOwnerAdded;
-          defaultText = literalExpression "regenerateOnOwnerAdded";
           type = bool;
           description = ''
-            Determines secret behavior when owners are removed from the configuration.
-
-            Typically mirrors regenerateOnOwnerAdded. Override cautiously.
-            Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
+            Whether the secret prefers to be rotated when the owners are removed, so the encrypted data
+            stored in fleet state can't be decrypted by those. Note that the secrets are still present in encrypted
+            form on those hosts until gc happens.
           '';
+          default = false;
         };
         allowDifferent = mkOption {
           type = bool;
           description = ''
-            When adding owner, do not update secret value for other owners, instead creating a new distribution
+            When adding owner, do not update secret value for other owners, instead creating a new distribution.
+
+            Defaults to true, since all secrets might differ on hosts on some point of deployment process.
+
+            Secret generator might also have opinion on this, like it makes little sense for askPass/synchronizing
+            generators to keep old data.
           '';
+          default = true;
         };
         generator = mkOption {
           type = uniq (nullOr (functionTo package));
@@ -75,6 +83,9 @@
     };
   };
   config = {
+    nixos = {host, ...}: {
+      _providedSharedSecrets = filter (name: elem host.name config.secrets.${name}.expectedOwners) (attrNames config.secrets);
+    };
     nixpkgs.overlays = [
       (final: prev: {
         mkSecretGenerators =
@@ -90,6 +101,7 @@
                 # (Some secrets-encryption-in-git/managed PKI solution is expected)
                 impureOn ? null,
                 generationData ? null,
+                allowDifferent ? true,
                 parts,
               }:
               (prev.writeShellScript "impureGenerator.sh" ''
@@ -118,7 +130,12 @@
               '').overrideAttrs
                 (old: {
                   passthru = {
-                    inherit impureOn parts generationData;
+                    inherit
+                      impureOn
+                      parts
+                      generationData
+                      allowDifferent
+                      ;
                     generatorKind = "impure";
                   };
                 });