difftreelog
feat attic
22 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5084,6 +5084,7 @@
"remowt-ui-prompt",
"tokio",
"tracing",
+ "tracing-journald",
"tracing-subscriber",
]
@@ -6576,6 +6577,17 @@
]
[[package]]
+name = "tracing-journald"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0"
+dependencies = [
+ "libc",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,6 +75,7 @@
tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
tracing = "0.1"
tracing-indicatif = "0.3.13"
+tracing-journald = "0.3.2"
tracing-opentelemetry = "0.33.0"
uuid = { version = "1", features = ["v4"] }
# For fixed coloring needs to be updated to https://github.com/tokio-rs/tracing/pull/3484
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -14,6 +14,8 @@
use tokio::task::spawn_blocking;
use tracing::{Instrument, error, field, info, info_span, warn};
+const TARGET: &str = "build-systems";
+
#[derive(Parser)]
pub struct Deploy {
/// Disable automatic rollback
@@ -32,7 +34,7 @@
}
async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<Utf8PathBuf> {
- info!("building");
+ info!(target: TARGET, "building");
let host = config.host(&hostname)?;
// let action = Action::from(self.subcommand.clone());
let nixos = host.nixos_config()?;
@@ -43,7 +45,7 @@
// We already have system profiles for backups.
if !host.local {
- info!("adding gc root");
+ info!(target: TARGET, "adding gc root");
let local = config.local_host();
let plugin_id = local.ensure_nix_plugin().await?;
let nix = local
@@ -70,7 +72,7 @@
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 span = info_span!(target: TARGET, "build", host = field::display(&host.name));
let hostname = host.name;
let build_attr = build_attr.clone();
tasks.push(
@@ -78,7 +80,7 @@
let built = match build_task(config, hostname.clone(), &build_attr).await {
Ok(path) => path,
Err(e) => {
- error!("failed to deploy host: {:?}", e);
+ error!(target: TARGET, "failed to deploy host: {:?}", e);
return;
}
};
@@ -86,9 +88,9 @@
let mut out = current_dir().expect("cwd exists");
out.push(format!("built-{hostname}"));
- info!("linking iso image to {:?}", out);
+ info!(target: TARGET, "linking iso image to {:?}", out);
if let Err(e) = symlink(built, out) {
- error!("failed to symlink: {e}")
+ error!(target: TARGET, "failed to symlink: {e}")
}
})
.instrument(span),
@@ -105,7 +107,7 @@
let tasks = FuturesUnordered::new();
for host in hosts.into_iter() {
let config = config.clone();
- let span = info_span!("deploy", host = field::display(&host.name));
+ let span = info_span!(target: TARGET, "deploy", host = field::display(&host.name));
let hostname = host.name.clone();
let opts = opts.clone();
if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind")? {
@@ -125,7 +127,7 @@
{
Ok(path) => path,
Err(e) => {
- error!("failed to build host system closure: {:?}", e);
+ error!(target: TARGET, "failed to build host system closure: {:?}", e);
return;
}
};
@@ -133,7 +135,7 @@
let deploy_kind = match host.deploy_kind().await {
Ok(v) => v,
Err(e) => {
- error!("failed to query target deploy kind: {e}");
+ error!(target: TARGET, "failed to query target deploy kind: {e}");
return;
}
};
@@ -141,7 +143,7 @@
// TODO: Make disable_rollback a host attribute instead
let mut disable_rollback = self.disable_rollback;
if !disable_rollback && deploy_kind != DeployKind::Fleet {
- warn!("disabling rollback, as not supported by non-fleet deployment kinds");
+ warn!(target: TARGET, "disabling rollback, as not supported by non-fleet deployment kinds");
disable_rollback = true;
}
@@ -149,7 +151,7 @@
match upload_task(&host, GenerationStorage::Deployer, built).await {
Ok(v) => v,
Err(e) => {
- error!("upload failed: {e}");
+ error!(target: TARGET, "upload failed: {e}");
return;
}
};
@@ -169,7 +171,7 @@
)
.await
{
- error!("activation failed: {e}");
+ error!(target: TARGET, "activation failed: {e}");
}
})
.instrument(span),
cmds/fleet/src/log_tree.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/fleet/src/log_tree.rs
@@ -0,0 +1,243 @@
+//! Streaming, indented tree formatter.
+//!
+//! Unlike `tracing-tree` it keeps no per-thread depth state, so interleaved
+//! parallel output stays correct; unlike `tracing-forest` it doesn't buffer
+//! whole trees, so logs stream live. Visual style mirrors `@meteor-it/logger`'s
+//! node receiver: a fixed-width, right-aligned, level-colored target column,
+//! then a magenta `>`/`<` gutter for span enter/exit (with duration on exit),
+//! and 2-space indentation per depth for events.
+
+use std::{
+ fmt::Write as _,
+ io::{IsTerminal as _, Write as _},
+ sync::{
+ Mutex,
+ atomic::{AtomicBool, Ordering},
+ },
+ time::{Duration, Instant},
+};
+
+use tracing::{Event, Level, Subscriber, field::Field, field::Visit, span::Attributes, span::Id};
+use tracing_subscriber::{
+ fmt::MakeWriter,
+ layer::{Context, Layer},
+ registry::{LookupSpan, Scope},
+};
+
+/// Width of the right-aligned target column.
+const NAME_LIMIT: usize = 18;
+
+#[derive(Default)]
+struct Fields {
+ message: String,
+ rest: String,
+}
+
+impl Visit for Fields {
+ fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
+ if field.name() == "message" {
+ let _ = write!(self.message, "{value:?}");
+ } else {
+ let _ = write!(self.rest, " {}={:?}", field.name(), value);
+ }
+ }
+}
+
+impl Fields {
+ fn render(&self) -> String {
+ format!("{}{}", self.message, self.rest).trim().to_owned()
+ }
+}
+
+/// Per-span state stashed in the registry's extensions.
+struct SpanData {
+ fields: String,
+ started: Instant,
+ // Whether the span's enter line was written, so we only emit an exit line
+ // for spans that actually appeared.
+ opened: AtomicBool,
+}
+
+/// SGR code → escape for the level-colored target column.
+fn level_code(level: &Level) -> &'static str {
+ match *level {
+ Level::ERROR => "31m", // red
+ Level::WARN => "33m", // yellow
+ Level::INFO => "34m", // blue
+ Level::DEBUG => "39m", // default fg
+ Level::TRACE => "90m", // gray
+ }
+}
+
+/// 2-space indent per depth, then a gutter symbol.
+fn gutter(depth: usize, sym: char) -> String {
+ let mut s = " ".repeat(depth);
+ s.push(sym);
+ s
+}
+
+fn fmt_dur(d: Duration) -> String {
+ if d.as_secs_f64() >= 1.0 {
+ format!("{:.2}s", d.as_secs_f64())
+ } else if d.as_millis() >= 1 {
+ format!("{}ms", d.as_millis())
+ } else {
+ format!("{}µs", d.as_micros())
+ }
+}
+
+pub struct TreeLayer<W> {
+ writer: W,
+ color: bool,
+ // Serializes the whole compute-and-write so enter/exit decisions, output
+ // ordering, and the writes stay consistent under parallel emission.
+ last: Mutex<Vec<Id>>,
+}
+
+impl<W> TreeLayer<W> {
+ pub fn new(writer: W) -> Self {
+ Self {
+ writer,
+ color: std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none(),
+ last: Mutex::new(vec![]),
+ }
+ }
+
+ /// Raw SGR escape, or empty when color is disabled.
+ fn sgr(&self, code: &str) -> String {
+ if self.color {
+ format!("\x1b[{code}")
+ } else {
+ String::new()
+ }
+ }
+
+ fn name_col(&self, mut target: &str, code: &str) -> String {
+ let w = NAME_LIMIT;
+ if target.len() > w {
+ target = &target[..w];
+ }
+ if self.color {
+ format!("\x1b[{code}\x1b[1m{target:>w$}\x1b[0m")
+ } else {
+ format!("{target:>w$}")
+ }
+ }
+
+ fn print_ancestors<S>(&self, scope: Option<Scope<'_, S>>, out: &mut String) -> usize
+ where
+ S: Subscriber + for<'a> LookupSpan<'a>,
+ {
+ let scope: Vec<_> = scope.map(|s| s.from_root().collect()).unwrap_or_default();
+
+ let mut _last = self.last.lock().expect("not poisoned");
+
+ let skip = scope
+ .iter()
+ .enumerate()
+ .take_while(|(i, s)| _last.get(*i) == Some(&s.id()))
+ .count();
+
+ for (depth, span) in scope.iter().enumerate().skip(skip) {
+ let ext = span.extensions();
+ let Some(data) = ext.get::<SpanData>() else {
+ continue;
+ };
+ let newly_open = !data.opened.swap(true, Ordering::Relaxed);
+ let _ = writeln!(
+ out,
+ "{} {}{}{} {}{}{}",
+ self.name_col(span.metadata().target(), "44m"),
+ self.sgr("35m"),
+ gutter(depth, if newly_open { '>' } else { '.' }),
+ self.sgr("1m"),
+ span.name(),
+ data.fields,
+ self.sgr("0m"),
+ );
+ }
+
+ *_last = scope.iter().map(|v| v.id()).collect();
+
+ scope.len()
+ }
+}
+
+impl<S, W> Layer<S> for TreeLayer<W>
+where
+ S: Subscriber + for<'a> LookupSpan<'a>,
+ W: for<'a> MakeWriter<'a> + 'static,
+{
+ fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
+ let span = ctx.span(id).expect("new span exists");
+ let mut fields = Fields::default();
+ attrs.record(&mut fields);
+ span.extensions_mut().insert(SpanData {
+ fields: fields.rest,
+ started: Instant::now(),
+ opened: AtomicBool::new(false),
+ });
+ }
+
+ fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
+ let mut out = String::new();
+
+ let scope = ctx.event_scope(event);
+ let pad = self.print_ancestors(scope, &mut out);
+
+ let mut fields = Fields::default();
+ event.record(&mut fields);
+ let message = fields.render();
+ let meta = event.metadata();
+ for (i, message) in message.split('\n').enumerate() {
+ // Event line: black bg + colored target column, then indent + message.
+ let _ = writeln!(
+ out,
+ "{}{}{}{}{}",
+ self.sgr("40m"),
+ self.name_col(
+ if i == 0 { meta.target() } else { "|" },
+ level_code(meta.level())
+ ),
+ self.sgr("0m"),
+ gutter(pad, ' '),
+ message,
+ );
+ }
+
+ let _ = self.writer.make_writer().write_all(out.as_bytes());
+ }
+
+ fn on_close(&self, id: Id, ctx: Context<'_, S>) {
+ let Some(span) = ctx.span(&id) else { return };
+ let (opened, dur) = span
+ .extensions()
+ .get::<SpanData>()
+ .map_or((false, Duration::ZERO), |d| {
+ (d.opened.load(Ordering::Relaxed), d.started.elapsed())
+ });
+ if !opened {
+ return;
+ }
+
+ let mut out = String::new();
+ let _pad = self.print_ancestors(ctx.span_scope(&id), &mut out);
+
+ let depth = span.scope().skip(1).count(); // ancestors == depth
+ let _ = writeln!(
+ out,
+ "{} {}{}{} {}{} (Done in {}){}",
+ self.name_col(span.metadata().target(), "44m"),
+ self.sgr("35m"),
+ gutter(depth, '<'),
+ self.sgr("1m"),
+ span.name(),
+ self.sgr("22m"),
+ fmt_dur(dur),
+ self.sgr("0m"),
+ );
+
+ let _guard = self.last.lock().expect("not poisoned");
+ let _ = self.writer.make_writer().write_all(out.as_bytes());
+ }
+}
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -1,6 +1,7 @@
#![recursion_limit = "512"]
pub(crate) mod cmds;
+mod log_tree;
use std::{process::ExitCode, sync::Arc};
@@ -21,6 +22,7 @@
use human_repr::HumanCount;
#[cfg(feature = "indicatif")]
use indicatif::{ProgressState, ProgressStyle};
+use log_tree::TreeLayer;
use nix_eval::{
eval_store, gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,
};
@@ -128,15 +130,21 @@
fn setup_logging(opts: &RootOpts) -> Result<()> {
#[cfg(feature = "indicatif")]
let indicatif_layer = {
+ use std::fmt;
use std::time::Duration;
IndicatifLayer::new().with_max_progress_bars(10, Some(ProgressStyle::default_spinner()))
+ .with_span_child_prefix_indent(" ")
+ .with_span_child_prefix_symbol("")
.with_progress_style(
ProgressStyle::with_template(
- "{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",
+ "{span_child_prefix:.magenta}{chevron:.magenta}{span_name:<18.blue.bold} {wide_msg} {span_fields:.dim} {color_start}{download_progress} {elapsed}{color_end}",
)
.unwrap()
- .with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
+ .with_key("chevron", |_: &ProgressState, writer: &mut dyn fmt::Write| {
+ let _ = write!(writer, "> ");
+ })
+ .with_key("download_progress", |state: &ProgressState, writer: &mut dyn fmt::Write| {
let Some(len) = state.len() else {
return;
};
@@ -149,7 +157,7 @@
})
.with_key(
"color_start",
- |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
+ |state: &ProgressState, writer: &mut dyn fmt::Write| {
let elapsed = state.elapsed();
if elapsed > Duration::from_secs(60) {
@@ -163,7 +171,7 @@
)
.with_key(
"color_end",
- |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
+ |state: &ProgressState, writer: &mut dyn fmt::Write| {
if state.elapsed() > Duration::from_secs(30) {
let _ = write!(writer, "\x1b[0m");
}
@@ -174,12 +182,15 @@
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
- let reg = tracing_subscriber::registry().with({
- let sub = tracing_subscriber::fmt::layer().without_time();
+ let tree = {
#[cfg(feature = "indicatif")]
- let sub = sub.with_writer(indicatif_layer.get_stderr_writer());
- sub.with_filter(filter) // .without,
- });
+ let writer = indicatif_layer.get_stderr_writer();
+ #[cfg(not(feature = "indicatif"))]
+ let writer = || std::io::stderr();
+ TreeLayer::new(writer)
+ };
+
+ let reg = tracing_subscriber::registry().with(tree.with_filter(filter));
#[cfg(feature = "indicatif")]
let reg = reg.with(indicatif_layer);
@@ -200,14 +211,12 @@
let logger = OpenTelemetryTracingBridge::new(&log_provider);
let tracer = span_provider.tracer("fleet");
- let reg = reg
- .with(tracing_opentelemetry::layer().with_tracer(tracer))
- .with(logger);
-
- reg.init();
+ reg.with(tracing_opentelemetry::layer().with_tracer(tracer))
+ .with(logger)
+ .init();
} else {
reg.init();
- };
+ }
Ok(())
}
@@ -252,7 +261,6 @@
.await
.expect("primary task panicked")
})
- // async_main(opts)
}
async fn main_real(opts: RootOpts) -> Result<()> {
crates/fleet-base/src/deploy.rsdiffbeforeafterboth--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -290,8 +290,10 @@
if !host.local {
info!("uploading system closure");
let mut tries = 0;
+ // TODO: Use gc prefix?..
loop {
- match host.remote_derivation(&generation).await {
+ let name = format!("{}-system", host.gc_root_prefix());
+ match host.remote_derivation(&generation, Some(&name)).await {
Ok(remote) => {
assert!(remote == generation, "CA derivations aren't implemented");
return Ok(remote);
crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -13,16 +13,16 @@
use camino::{Utf8Path, Utf8PathBuf};
use chrono::{DateTime, Utc};
use fleet_shared::SecretData;
-use nix_eval::{Store, Value, drv::DrvGraph, eval_store, nix_go, nix_go_json, util::assert_warn};
+use nix_eval::{Store, Value, eval_store, nix_go, nix_go_json, util::assert_warn};
use remowt_client::{AgentBundle, Remowt};
use remowt_endpoints::fs::FsClient;
use remowt_fleet::NixClient;
-use remowt_link_shared::BifConfig;
+use remowt_link_shared::{Address, BifConfig};
use remowt_ui_prompt::auto::AutoPrompter;
use remowt_ui_prompt::bifrost::PromptEndpoints;
use remowt_ui_prompt::{PrependSourcePrompter, Source};
use tempfile::NamedTempFile;
-use tokio::{sync::OnceCell, task::spawn_blocking};
+use tokio::{process::Command, sync::OnceCell, task::spawn_blocking};
use tracing::{info, instrument, warn};
use crate::fleetdata::{
@@ -104,11 +104,14 @@
session_destination: OnceLock<String>,
legacy_ssh_store: OnceLock<bool>,
+ /// fleetConfiguration.hosts.host, None for local (as local is only used for local tool overrides)
pub host_config: Option<Value>,
pub nixos_config: OnceLock<Value>,
pub nixos_unchecked_config: OnceLock<Value>,
pub pkgs_override: Option<Value>,
+ binary_cache: OnceCell<Option<BinaryCache>>,
+
// TODO: Move command helpers away with connectivity refactor
pub local: bool,
pub remowt: OnceCell<Remowt>,
@@ -131,13 +134,35 @@
AgentBundle::from_dir(agents_dir()?)
}
-enum RemoteDerivationMode {
- /// Closure is (pre)signed, remote host is expected to trust our signatures.
- CopySigned,
- /// Closure is not signed, using escalated nix daemon to bypass signature verification.
- CopyPrivileged,
- /// Closure is uploaded to attic, remote host is expected to trust its signature.
- Attic { name: String },
+#[derive(Clone)]
+pub enum BinaryCache {
+ Attic { name: String, pin: bool },
+}
+impl BinaryCache {
+ async fn upload(&self, local: &Remowt, path: &Utf8Path, pin_name: Option<&str>) -> Result<()> {
+ match self {
+ BinaryCache::Attic { name, pin } => {
+ let mut cmd = local.cmd("attic");
+ cmd.arg("push").arg(name).arg(path.as_str());
+ cmd.run()
+ .await
+ .context("failed to push path to attic (is attic-cli on PATH?)")?;
+
+ if *pin && let Some(pin_name) = pin_name {
+ let mut cmd = local.cmd("attic");
+ cmd.arg("pin")
+ .arg("create")
+ .arg(name)
+ .arg(pin_name)
+ .arg(path.as_str());
+ cmd.run()
+ .await
+ .context("failed to pin path in attic (is your attic-cli compatible?)")?;
+ }
+ Ok(())
+ }
+ }
+ }
}
impl ConfigHost {
@@ -156,6 +181,32 @@
.set(legacy)
.expect("legacy ssh store is already set")
}
+ pub fn binary_cache(&self) -> Result<Option<BinaryCache>> {
+ if let Some(v) = self.binary_cache.get() {
+ return Ok(v.clone());
+ }
+ let cache = if let Some(host_config) = &self.host_config {
+ let cache = nix_go!(host_config.cache);
+ let enable: bool = nix_go_json!(cache.enable);
+ if enable {
+ let kind: String = nix_go_json!(cache.kind);
+ let name: String = nix_go_json!(cache.name);
+ match kind.as_str() {
+ "attic" => {
+ let pin: bool = nix_go_json!(cache.pin);
+ Some(BinaryCache::Attic { name, pin })
+ }
+ v => bail!("unknown binary cache kind: {v}"),
+ }
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+ let _ = self.binary_cache.set(cache.clone());
+ Ok(cache)
+ }
pub async fn deploy_kind(&self) -> Result<DeployKind> {
self.deploy_kind.get_or_try_init(|| async {
let remowt = self.remowt().await?;
@@ -238,8 +289,10 @@
let built = plugin
.build("out")
.context("failed to build the fleet nix plugin")?;
+ let pin = format!("{}-remowt-plugin-fleet", self.gc_root_prefix());
let copied = self
- .remote_derivation(&built)
+ // TODO: Pin? But this pin will be host-specific, and heavy because of nix dependency...
+ .remote_derivation(&built, Some(&pin))
.await
.context("failed to copy the fleet nix plugin to the host store")?;
let bin = copied.join("bin/remowt-plugin-fleet");
@@ -248,6 +301,13 @@
.run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())
.await
.context("failed to load the fleet nix plugin")?;
+ // Should not fail, as plugin loading waits for connection on remote, and we only wait for forward... On the other hand, we should monitor connection to the remote itself somewhere too.
+ self.remowt()
+ .await?
+ .rpc()
+ .wait_for_connection_to(Address::Plugin(NIX_PLUGIN_ID))
+ .await
+ .map_err(|_| anyhow!("failed to wait"))?;
anyhow::Ok(())
})
.await?;
@@ -339,17 +399,31 @@
}
/// Returns path for futureproofing, as path might change i.e on conversion to CA
#[instrument(skip(self))]
- pub async fn remote_derivation(&self, path: &Utf8Path) -> Result<Utf8PathBuf> {
+ pub async fn remote_derivation(
+ &self,
+ path: &Utf8Path,
+ pin_name: Option<&str>,
+ ) -> Result<Utf8PathBuf> {
let path = path.to_owned();
if self.local {
// Path is located locally, thus already trusted.
return Ok(path);
}
+ let cache = self.binary_cache()?;
+ let mut cache = if let Some(cache) = cache {
+ let local = self.config.local_host().remowt().await?;
+ let path = path.to_owned();
+ let pin_name = pin_name.map(str::to_owned);
+ Some(tokio::spawn(async move {
+ if let Err(e) = cache.upload(&local, &path, pin_name.as_deref()).await {
+ warn!("failed to upload closure {path} to cache: {e:?}");
+ }
+ }))
+ } else {
+ None
+ };
let sign: Pin<Box<dyn Future<Output = Result<()>> + Send>> = {
let path = path.clone();
- let graph = DrvGraph::resolve(&eval_store(), &path)
- .context("failed to resolve graph to be uploaded")?;
- info!("signing {} paths", graph.nodes.len());
Box::pin(async move {
let local = self.config.local_host();
@@ -377,13 +451,62 @@
.expect("copy_to panicked")
.context("copying closure to remote store")?;
}
+
+ if let Some(cache) = cache.take() {
+ let _ = cache.await;
+ }
+
Ok(path)
}
+
+ /// Like [`Self::remote_derivation`], but instead of copying the closure over
+ /// the nix daemon tunnel, uploads it to an attic binary cache and has the
+ /// remote host substitute it from there. The remote is expected to already
+ /// trust the cache's signing key and have it configured as a substituter.
+ #[instrument(skip(self))]
+ pub async fn remote_derivation_attic(
+ &self,
+ path: &Utf8Path,
+ cache_name: &str,
+ ) -> Result<Utf8PathBuf> {
+ let path = path.to_owned();
+ if self.local {
+ // Path is located locally, thus already trusted.
+ return Ok(path);
+ }
+
+ info!("uploading {path} closure to attic cache {cache_name}");
+ let status = Command::new("attic")
+ .arg("push")
+ .arg(cache_name)
+ .arg(path.as_str())
+ .status()
+ .await
+ .context("failed to spawn attic-cli, is it on PATH?")?;
+ ensure!(status.success(), "attic push failed: {status}");
+
+ info!("substituting {path} on the remote host");
+ let nix = self.nix_client().await?;
+ let substituted = nix
+ .substitute(vec![path.clone()])
+ .await
+ .map_err(|e| anyhow!("{e:?}"))?
+ .map_err(|e| anyhow!("{e}"))?;
+ ensure!(
+ substituted.contains(&path),
+ "remote host failed to substitute {path} from attic cache {cache_name}"
+ );
+ Ok(path)
+ }
}
struct HostSecretDefinition(Value);
impl ConfigHost {
+ pub fn gc_root_prefix(&self) -> String {
+ format!("{}-{}", self.config.gc_root_prefix(), self.name)
+ }
+
// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,
// assuming getting tags always returns the same value.
pub fn tags(&self) -> Result<Vec<String>> {
@@ -504,6 +627,7 @@
cell
},
pkgs_override: Some(self.default_pkgs.clone()),
+ binary_cache: OnceCell::new(),
local: true,
remowt: OnceCell::new(),
@@ -546,6 +670,7 @@
nixos_unchecked_config: OnceLock::new(),
groups: OnceLock::new(),
pkgs_override: None,
+ binary_cache: OnceCell::new(),
// TODO: Remove with connectivit refactor
local: self.localhost == name,
crates/fleet-base/src/keys.rsdiffbeforeafterboth--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -9,6 +9,9 @@
use crate::{fleetdata::SecretOwner, host::Config};
impl Config {
+ pub fn gc_root_prefix(&self) -> String {
+ self.data.gc_root_prefix.clone()
+ }
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);
crates/fleet-base/src/primops.rsdiffbeforeafterboth--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -119,7 +119,7 @@
.context("failed to build generator for target host")?;
let generator = host_on
- .remote_derivation(&generator)
+ .remote_derivation(&generator, None)
.await
.context("failed to copy generator to target host")?;
crates/nix-eval/src/lib.rsdiffbeforeafterboth1use std::borrow::Cow;2use std::cell::RefCell;3use std::collections::HashMap;4use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};5use std::ptr::{null, null_mut};6use std::sync::{Arc, LazyLock, OnceLock};7use std::{array, fmt, slice};89use anyhow::{Context, anyhow, bail};10use camino::{Utf8Path, Utf8PathBuf};11use itertools::Itertools;12use serde::Serialize;13use serde::de::DeserializeOwned;14use std::mem::transmute;1516pub use anyhow::Result;17use tracing::{Span, instrument, warn};1819use self::logging::{ErrorInfoBuilder, nix_logging_cxx};20use self::nix_cxx::set_fetcher_setting;21use self::nix_raw::{22 BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,23 GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base,24 GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,25 PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,26 bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,27 clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,28 err_NIX_ERR_UNKNOWN, err_NIX_OK, err_code, err_info_msg, err_msg, eval_state_build,29 eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,30 expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,31 flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,32 flake_reference_and_fragment_from_string, flake_reference_parse_flags,33 flake_reference_parse_flags_free, flake_reference_parse_flags_new,34 flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,35 flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size,36 get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int,37 init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free,38 list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs,39 make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,40 realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,41 realised_string_get_store_path, realised_string_get_store_path_count, register_primop,42 set_err_msg, setting_get, setting_set, state_free, store_copy_closure, store_free, store_open,43 store_parse_path, store_path_free, store_path_name, string_realise, value, value_call,44 value_decref, value_force, value_incref,45};4647// Contains macros helpers48pub mod drv;49pub mod logging;50#[doc(hidden)]51pub mod macros;52pub mod scheduler;5354#[doc(hidden)]55pub mod __macro_support {56 pub use std::collections::hash_map::HashMap;5758 pub use anyhow::Context;59 pub use tokio::task::block_in_place;60}61pub mod util;6263#[allow(64 non_upper_case_globals,65 non_camel_case_types,66 non_snake_case,67 dead_code68)]69mod nix_raw {70 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));71}72#[cxx::bridge]73pub mod nix_cxx {74 struct AddFileToStoreResult {75 error: String,76 store_path: String,77 hash: String,78 }79 struct CxxProfileGeneration {80 id: u64,81 store_path: String,82 creation_time_unix: i64,83 current: bool,84 }85 struct CxxListGenerationsResult {86 error: String,87 generations: Vec<CxxProfileGeneration>,88 }89 struct CxxBuildResult {90 error: String,91 outputs: Vec<String>,92 }93 unsafe extern "C++" {94 type nix_fetchers_settings;95 type Store;96 include!("nix-eval/src/lib.hh");9798 #[allow(clippy::missing_safety_doc)]99 unsafe fn set_fetcher_setting(100 settings: *mut nix_fetchers_settings,101 setting: *const c_char,102 value: *const c_char,103 );104105 #[allow(clippy::missing_safety_doc)]106 unsafe fn switch_profile(store: *mut Store, profile: &str, store_path: &str) -> String;107108 #[allow(clippy::missing_safety_doc)]109 unsafe fn sign_closure(store: *mut Store, store_path: &str, key_file: &str) -> String;110111 #[allow(clippy::missing_safety_doc)]112 unsafe fn add_file_to_store(113 store: *mut Store,114 name: &str,115 path: &str,116 ) -> AddFileToStoreResult;117118 fn list_generations(profile_path: &str) -> CxxListGenerationsResult;119120 #[allow(clippy::missing_safety_doc)]121 unsafe fn build_drv_outputs(122 store: *mut Store,123 drv_path: &str,124 output_names_joined: &str,125 ) -> CxxBuildResult;126127 #[allow(clippy::missing_safety_doc)]128 unsafe fn substitute_paths(store: *mut Store, paths_joined: &str) -> CxxBuildResult;129130 #[allow(clippy::missing_safety_doc)]131 unsafe fn is_valid_path(store: *mut Store, path: &str) -> bool;132 }133}134135#[derive(Debug, PartialEq, Eq)]136pub enum NixType {137 Thunk,138 Int,139 Float,140 Bool,141 String,142 Path,143 Null,144 Attrs,145 List,146 Function,147 External,148}149impl NixType {150 fn from_int(c: c_uint) -> Self {151 match c {152 0 => Self::Thunk,153 1 => Self::Int,154 2 => Self::Float,155 3 => Self::Bool,156 4 => Self::String,157 5 => Self::Path,158 6 => Self::Null,159 7 => Self::Attrs,160 8 => Self::List,161 9 => Self::Function,162 10 => Self::External,163 _ => unreachable!("unknown nix type: {c}"),164 }165 }166}167168enum FunctorKind {169 Function,170 Functor,171}172173#[derive(Debug)]174#[repr(i32)]175pub enum NixErrorKind {176 Unknown = err_NIX_ERR_UNKNOWN,177 Overflow = err_NIX_ERR_OVERFLOW,178 Key = err_NIX_ERR_KEY,179 Generic = err_NIX_ERR_NIX_ERROR,180}181impl NixErrorKind {182 fn from_int(v: c_int) -> Option<Self> {183 Some(match v {184 0 => return None,185 nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,186 nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,187 nix_raw::err_NIX_ERR_KEY => Self::Key,188 nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,189 _ => {190 debug_assert!(false, "unexpected nix error kind: {v}");191 Self::Unknown192 }193 })194 }195}196197pub fn gc_now() {198 unsafe { gc_now_raw() };199}200201pub fn gc_register_my_thread() {202 assert_eq!(unsafe { GC_thread_is_registered() }, 0);203204 let mut sb = GC_stack_base {205 mem_base: null_mut(),206 };207 let r = unsafe { GC_get_stack_base(&mut sb) };208 if r as u32 != GC_SUCCESS {209 panic!("failed to get thread stack base");210 }211 unsafe { GC_register_my_thread(&sb) };212}213pub fn gc_unregister_my_thread() {214 assert_eq!(unsafe { GC_thread_is_registered() }, 1);215216 unsafe { GC_unregister_my_thread() };217}218219pub struct ThreadRegisterGuard {}220impl ThreadRegisterGuard {221 #[allow(clippy::new_without_default)]222 pub fn new() -> Self {223 gc_register_my_thread();224 Self {}225 }226}227impl Drop for ThreadRegisterGuard {228 fn drop(&mut self) {229 gc_unregister_my_thread();230 }231}232233#[repr(transparent)]234pub struct NixContext(*mut c_context);235impl NixContext {236 pub fn set_err_raw(&mut self, err: NixErrorKind, msg: &CStr) {237 unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };238 }239 pub fn set_err(&mut self, err: anyhow::Error) {240 let fmt = format!("{err:?}").replace("\0", "\\0");241 self.set_err_raw(242 NixErrorKind::Generic,243 &CString::new(fmt).expect("NUL bytes were just replaced"),244 );245 }246 pub fn new() -> Self {247 let ctx = unsafe { c_context_create() };248 Self(ctx)249 }250 fn error_kind(&self) -> Option<NixErrorKind> {251 let code = unsafe { err_code(self.0) };252 NixErrorKind::from_int(code)253 }254 fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {255 if let NixErrorKind::Generic = self.error_kind()? {256 let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };257 let mut err_out = String::new();258 unsafe {259 err_info_msg(260 null_mut(),261 self.0,262 Some(copy_nix_str),263 (&raw mut err_out).cast(),264 )265 };266 return Some((Cow::Owned(err_out), Some(ei)));267 };268269 // TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,270 // but it looks ugly271 let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };272 Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))273 }274 fn clean_err(&mut self) {275 unsafe {276 clear_err(self.0);277 }278 }279280 fn bail_if_error(&self) -> Result<()> {281 if let Some((err, stack)) = self.error() {282 let mut e = Err(anyhow!("{err}"));283 if let Some(stack) = stack {284 for ele in stack.stack_frames {285 e = e.with_context(|| {286 if ele.pos.is_empty() {287 ele.msg288 } else {289 format!("{} at {}", ele.msg, ele.pos)290 }291 })292 }293 }294 return e.context("<nix frames>");295 };296 Ok(())297 }298299 fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {300 self.clean_err();301 let o = f(self.0);302 self.bail_if_error()?;303 self.clean_err();304 Ok(o)305 }306}307308impl Default for NixContext {309 fn default() -> Self {310 Self::new()311 }312}313impl Drop for NixContext {314 fn drop(&mut self) {315 unsafe {316 c_context_free(self.0);317 }318 }319}320struct GlobalState {321 // Store should be valid as long as EvalState is valid322 #[allow(dead_code)]323 store: Arc<Store>,324 state: EvalState,325}326impl GlobalState {327 fn new() -> Result<Self> {328 let mut ctx = NixContext::new();329 let store = Arc::new(330 ctx.run_in_context(|c| unsafe { store_open(c, c"auto".as_ptr(), null_mut()) })331 .map(Store)?,332 );333334 let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;335 ctx.run_in_context(|c| unsafe { eval_state_builder_load(c, builder) })?;336 ctx.run_in_context(|c| unsafe {337 eval_state_builder_set_eval_setting(338 c,339 builder,340 c"lazy-trees".as_ptr(),341 c"true".as_ptr(),342 )343 })?;344 ctx.run_in_context(|c| unsafe {345 eval_state_builder_set_eval_setting(346 c,347 builder,348 c"lazy-locks".as_ptr(),349 c"true".as_ptr(),350 )351 })?;352 let state = ctx353 .run_in_context(|c| unsafe { eval_state_build(c, builder) })354 .map(EvalState)?;355356 Ok(Self { store, state })357 }358}359360struct ThreadState {361 ctx: NixContext,362}363impl ThreadState {364 fn new() -> Result<Self> {365 let ctx = NixContext::new();366367 Ok(Self { ctx })368 }369}370371static GLOBAL_STATE: LazyLock<GlobalState> =372 LazyLock::new(|| GlobalState::new().expect("global state init shouldn't fail"));373374thread_local! {375 static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));376}377pub(crate) fn with_default_context<T>(378 f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T,379) -> Result<T> {380 let global = &GLOBAL_STATE.state;381 let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));382 let mut ctx = NixContext(ctx);383 let v = ctx.run_in_context(|c| f(c, state));384 // It is reused for thread385 std::mem::forget(ctx);386 v387}388389pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> {390 with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())391}392393pub fn get_setting(s: &CStr) -> Result<String> {394 let mut out = String::new();395 with_default_context(|c, _| unsafe {396 setting_get(c, s.as_ptr(), Some(copy_nix_str), (&raw mut out).cast())397 })?;398 Ok(out)399}400401#[derive(Debug)]402pub struct AddedFile {403 pub store_path: Utf8PathBuf,404 pub hash: String,405}406407#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]408pub struct ProfileGeneration {409 pub id: u64,410 pub store_path: Utf8PathBuf,411 pub creation_time_unix: i64,412 pub current: bool,413}414415#[instrument]416pub fn list_generations(profile_path: &str) -> Result<Vec<ProfileGeneration>> {417 let res = nix_cxx::list_generations(profile_path);418 if !res.error.is_empty() {419 bail!(420 "failed to list generations at {profile_path}: {}",421 res.error422 );423 }424 Ok(res425 .generations426 .into_iter()427 .map(|g| ProfileGeneration {428 id: g.id,429 store_path: Utf8PathBuf::from(g.store_path),430 creation_time_unix: g.creation_time_unix,431 current: g.current,432 })433 .collect())434}435436pub struct FetchSettings(*mut fetchers_settings);437impl FetchSettings {438 pub fn new() -> Self {439 Self::try_new().expect("allocation should not fail")440 }441 fn try_new() -> Result<Self> {442 with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)443 }444 pub fn set(&mut self, setting: &CStr, value: &CStr) {445 unsafe {446 set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());447 };448 }449}450unsafe impl Send for FetchSettings {}451unsafe impl Sync for FetchSettings {}452453impl Default for FetchSettings {454 fn default() -> Self {455 Self::new()456 }457}458459impl Drop for FetchSettings {460 fn drop(&mut self) {461 unsafe { fetchers_settings_free(self.0) };462 }463}464pub struct FlakeSettings(*mut flake_settings);465impl FlakeSettings {466 pub fn new() -> Result<Self> {467 with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)468 }469}470unsafe impl Send for FlakeSettings {}471unsafe impl Sync for FlakeSettings {}472impl Drop for FlakeSettings {473 fn drop(&mut self) {474 unsafe {475 flake_settings_free(self.0);476 }477 }478}479480pub struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);481impl FlakeReferenceParseFlags {482 pub fn new(settings: &FlakeSettings) -> Result<Self> {483 with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })484 .map(Self)485 }486 pub fn set_base_dir(&mut self, dir: &str) -> Result<()> {487 with_default_context(|c, _| {488 unsafe {489 flake_reference_parse_flags_set_base_directory(490 c,491 self.0,492 dir.as_ptr().cast(),493 dir.len(),494 )495 };496 })497 }498}499impl Drop for FlakeReferenceParseFlags {500 fn drop(&mut self) {501 unsafe {502 flake_reference_parse_flags_free(self.0);503 }504 }505}506pub struct FlakeLockFlags(*mut flake_lock_flags);507impl FlakeLockFlags {508 pub fn new(settings: &FlakeSettings) -> Result<Self> {509 let o = with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) })510 .map(Self)?;511 // with_default_context(|c, _| unsafe { flake_lock_flags_set_mode_virtual(c, o.0) })?;512513 Ok(o)514 }515}516impl Drop for FlakeLockFlags {517 fn drop(&mut self) {518 unsafe {519 flake_lock_flags_free(self.0);520 }521 }522}523524pub(crate) unsafe extern "C" fn copy_nix_str(525 start: *const c_char,526 n: c_uint,527 user_data: *mut c_void,528) {529 let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };530 let s = std::str::from_utf8(s).expect("c string has invalid utf-8");531 unsafe { *user_data.cast::<String>() = s.to_owned() };532}533534pub struct Store(*mut c_store);535unsafe impl Send for Store {}536unsafe impl Sync for Store {}537538pub fn eval_store() -> Arc<Store> {539 GLOBAL_STATE.store.clone()540}541542impl Store {543 pub fn open(uri: &str) -> Result<Self> {544 let uri = CString::new(uri)?;545 let ptr = with_default_context(|c, _| unsafe { store_open(c, uri.as_ptr(), null_mut()) })?;546 if ptr.is_null() {547 bail!("failed to open store");548 }549 Ok(Store(ptr))550 }551552 pub fn parse_path(&self, path: &Utf8Path) -> Result<StorePath> {553 let path = CString::new(path.as_str()).expect("valid cstr");554 with_default_context(|c, _| {555 StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })556 })557 }558559 #[instrument(skip(self))]560 pub fn sign_closure(&self, path: &Utf8Path, key_file: &Utf8Path) -> Result<()> {561 let err = with_default_context(|_, _| unsafe {562 nix_cxx::sign_closure(self.as_ptr().cast(), path.as_str(), key_file.as_str())563 })?564 .to_string();565566 if err.is_empty() {567 Ok(())568 } else {569 bail!("failed to sign {path}: {err}");570 }571 }572573 #[instrument(skip(self, dst))]574 pub fn copy_to(&self, dst: &Store, path: &Utf8Path) -> Result<()> {575 let sp = self576 .parse_path(path)577 .context("failed to parse store path")?;578 let rc = with_default_context(|c, _| unsafe {579 store_copy_closure(c, self.as_ptr(), dst.0, sp.as_ptr())580 })?;581 if rc != err_NIX_OK {582 bail!("store_copy_closure failed (code {rc})");583 }584 Ok(())585 }586587 /// Would only work with local store.588 #[instrument(skip(self))]589 pub fn switch_profile(&self, profile: &str, path: &Utf8Path) -> Result<()> {590 let msg = unsafe { nix_cxx::switch_profile(self.as_ptr().cast(), profile, path.as_str()) };591 if msg.is_empty() {592 Ok(())593 } else {594 bail!("failed to switch profile {profile}: {msg}");595 }596 }597598 #[instrument(skip(self))]599 pub fn add_file(&self, name: &str, path: &Utf8Path) -> Result<AddedFile> {600 let msg = unsafe { nix_cxx::add_file_to_store(self.as_ptr().cast(), name, path.as_str()) };601 if !msg.error.is_empty() {602 bail!("failed to add {path} to store: {}", msg.error)603 }604 Ok(AddedFile {605 store_path: Utf8PathBuf::from(msg.store_path),606 hash: msg.hash,607 })608 }609610 #[instrument(skip(self, paths))]611 pub fn substitute_paths(&self, paths: &[Utf8PathBuf]) -> Result<Vec<Utf8PathBuf>> {612 let joined = paths.into_iter().join("\n");613 let res = unsafe { nix_cxx::substitute_paths(self.as_ptr().cast(), &joined) };614 if !res.error.is_empty() {615 warn!("substitute_paths reported: {}", res.error);616 }617 Ok(res.outputs.into_iter().map(Utf8PathBuf::from).collect())618 }619620 #[instrument(skip(self))]621 pub fn is_valid_path(&self, path: &Utf8Path) -> bool {622 unsafe { nix_cxx::is_valid_path(self.as_ptr().cast(), path.as_str()) }623 }624625 #[instrument(skip(self))]626 pub fn build_drv_outputs(627 &self,628 drv_path: &Utf8Path,629 output_names: &[String],630 ) -> Result<Vec<String>> {631 let joined = output_names.join("\n");632 let res =633 unsafe { nix_cxx::build_drv_outputs(self.as_ptr().cast(), drv_path.as_str(), &joined) };634 if !res.error.is_empty() {635 bail!("build of {drv_path} failed: {}", res.error);636 }637 Ok(res.outputs)638 }639640 #[instrument(skip(self))]641 pub fn store_dir(&self) -> Result<Utf8PathBuf> {642 let mut out = String::new();643 with_default_context(|c, es| unsafe {644 nix_raw::store_get_storedir(c, self.as_ptr(), Some(copy_nix_str), (&raw mut out).cast())645 })?;646 let p = Utf8PathBuf::from(out);647 assert!(p.is_absolute());648 Ok(p)649 }650651 fn as_ptr(&self) -> *mut c_store {652 self.0653 }654}655impl Drop for Store {656 fn drop(&mut self) {657 unsafe { store_free(self.0) }658 }659}660661#[repr(transparent)]662pub struct EvalState(*mut c_eval_state);663unsafe impl Send for EvalState {}664unsafe impl Sync for EvalState {}665666impl Drop for EvalState {667 fn drop(&mut self) {668 unsafe {669 state_free(self.0);670 }671 }672}673674pub struct FlakeReference(*mut flake_reference);675impl FlakeReference {676 #[instrument(name = "new-flake-reference", skip(flake, parse, fetch))]677 pub fn new(678 s: &str,679 flake: &FlakeSettings,680 parse: &FlakeReferenceParseFlags,681 fetch: &FetchSettings,682 ) -> Result<(Self, String)> {683 let mut out = null_mut();684 let mut fragment = String::new();685 // let fetch_settings = fetcher_settings;686 with_default_context(|c, _| unsafe {687 flake_reference_and_fragment_from_string(688 c,689 fetch.0,690 flake.0,691 parse.0,692 s.as_ptr().cast(),693 s.len(),694 &mut out,695 Some(copy_nix_str),696 (&raw mut fragment).cast(),697 )698 })?;699 assert!(!out.is_null());700701 Ok((Self(out), fragment))702 }703 #[instrument(name = "lock-flake", skip(self, fetch, flake, lock))]704 pub fn lock(705 &mut self,706 fetch: &FetchSettings,707 flake: &FlakeSettings,708 lock: &FlakeLockFlags,709 ) -> Result<LockedFlake> {710 with_default_context(|c, es| unsafe { flake_lock(c, fetch.0, flake.0, es, lock.0, self.0) })711 .map(LockedFlake)712 }713}714unsafe impl Send for FlakeReference {}715unsafe impl Sync for FlakeReference {}716717pub struct LockedFlake(*mut locked_flake);718impl LockedFlake {719 pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {720 with_default_context(|c, es| unsafe {721 locked_flake_get_output_attrs(c, settings.0, es, self.0)722 })723 .map(Value)724 }725}726unsafe impl Send for LockedFlake {}727unsafe impl Sync for LockedFlake {}728impl Drop for LockedFlake {729 fn drop(&mut self) {730 unsafe {731 locked_flake_free(self.0);732 };733 }734}735736type FieldName = [u8; 64];737fn init_field_name(v: &str) -> FieldName {738 let mut f = [0; 64];739 assert!(v.len() < 64, "max field name is 63 chars");740 assert!(741 v.bytes().all(|v| v != 0),742 "nul bytes are unsupported in field name"743 );744 f[0..v.len()].copy_from_slice(v.as_bytes());745 f746}747748pub struct RealisedString(*mut realised_string);749impl fmt::Debug for RealisedString {750 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {751 self.as_str().fmt(f)752 }753}754755impl RealisedString {756 pub fn as_str(&self) -> &str {757 let len = unsafe { realised_string_get_buffer_size(self.0) };758 let data: *const u8 = unsafe { realised_string_get_buffer_start(self.0) }.cast();759 let data = unsafe { slice::from_raw_parts(data, len) };760 std::str::from_utf8(data).expect("non-utf8 strings not supported")761 }762 pub fn path_count(&self) -> usize {763 unsafe { realised_string_get_store_path_count(self.0) }764 }765 pub fn path(&self, i: usize) -> String {766 assert!(i < self.path_count());767 let path = unsafe { realised_string_get_store_path(self.0, i) };768 let mut err_out = String::new();769 unsafe { store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };770 err_out771 }772}773774unsafe impl Send for RealisedString {}775impl Drop for RealisedString {776 fn drop(&mut self) {777 unsafe { realised_string_free(self.0) }778 }779}780781#[repr(transparent)]782pub struct Value(*mut value);783784unsafe impl Send for Value {}785unsafe impl Sync for Value {}786787pub trait AsFieldName {788 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;789 fn to_field_name(&self) -> Result<String>;790}791impl AsFieldName for Value {792 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {793 let f = self.to_string()?;794 v(init_field_name(&f))795 }796 fn to_field_name(&self) -> Result<String> {797 self.to_string()798 }799}800impl<E> AsFieldName for E801where802 E: AsRef<str>,803{804 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {805 let f = self.as_ref();806 v(init_field_name(f))807 }808 fn to_field_name(&self) -> Result<String> {809 Ok(self.as_ref().to_owned())810 }811}812813struct AttrsBuilder(*mut c_bindings_builder);814impl AttrsBuilder {815 fn new(capacity: usize) -> Self {816 with_default_context(|c, es| unsafe { make_bindings_builder(c, es, capacity) })817 .map(Self)818 .expect("alloc should not fail")819 }820 fn insert(&mut self, k: &impl AsFieldName, v: Value) {821 k.as_field_name(|name| {822 with_default_context(|c, _| unsafe {823 bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0);824 // bindings_builder_insert doesn't do incref825 })826 })827 .expect("builder insert shouldn't fail");828 }829}830impl Drop for AttrsBuilder {831 fn drop(&mut self) {832 unsafe { bindings_builder_free(self.0) };833 }834}835836struct ListBuilder(*mut c_list_builder, c_uint);837impl ListBuilder {838 fn new(capacity: usize) -> Self {839 with_default_context(|c, es| unsafe { make_list_builder(c, es, capacity) })840 .map(|l| Self(l, 0))841 .expect("alloc should not fail")842 }843}844impl ListBuilder {845 fn push(&mut self, v: Value) {846 with_default_context(|c, _| unsafe {847 list_builder_insert(848 c,849 self.0,850 {851 let v = self.1;852 self.1 += 1;853 v854 },855 v.0,856 )857 })858 .expect("list insert shouldn't fail");859 }860}861impl Drop for ListBuilder {862 fn drop(&mut self) {863 unsafe { list_builder_free(self.0) };864 }865}866867impl Value {868 pub fn new_primop(v: NativeFn) -> Self {869 let out = Self::new_uninit();870 with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) })871 .expect("primop initialization should not fail");872 out873 }874 pub fn new_attrs(v: HashMap<&str, Value>) -> Self {875 let out = Self::new_uninit();876 let mut b = AttrsBuilder::new(v.len());877 for (k, v) in v {878 b.insert(&k, v);879 }880 with_default_context(|c, _| unsafe { make_attrs(c, out.0, b.0) })881 .expect("attrs initialization should not fail");882883 out884 }885 fn new_list<T: Into<Self>>(v: Vec<T>) -> Self {886 let out = Self::new_uninit();887 let mut b = ListBuilder::new(v.len());888 for v in v {889 b.push(v.into());890 }891 with_default_context(|c, _| unsafe { make_list(c, b.0, out.0) })892 .expect("list initialization should not fail");893894 out895 }896 fn new_uninit() -> Self {897 let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })898 .expect("value allocation should not fail");899 Self(out)900 }901 pub fn new_str(v: &str) -> Self {902 let s = CString::new(v).expect("string should not contain NULs");903 let out = Self::new_uninit();904 // String is copied, `s` is free to be dropped905 with_default_context(|c, _| unsafe { init_string(c, out.0, s.as_ptr()) })906 .expect("string initialization should not fail");907 out908 }909 pub fn new_int(i: i64) -> Self {910 let out = Self::new_uninit();911 with_default_context(|c, _| unsafe { init_int(c, out.0, i) })912 .expect("int initialization should not fail");913 out914 }915 pub fn new_bool(v: bool) -> Self {916 let out = Self::new_uninit();917 with_default_context(|c, _| unsafe { init_bool(c, out.0, v) })918 .expect("bool initialization should not fail");919 out920 }921 // TODO: As far as I can see, there is no way to get Thunks from nix public C api, so this function is useless922 // fn force(&mut self, st: &mut EvalState) -> Result<()> {923 // with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;924 // Ok(())925 // }926 pub fn type_of(&self) -> NixType {927 let ty = with_default_context(|c, _| unsafe { get_type(c, self.0) })928 .expect("get_type should not fail");929 NixType::from_int(ty)930 }931 fn builtin_to_string(&self) -> Result<Self> {932 let builtin = Self::eval("builtins.toString")?;933 builtin.call(self.clone())934 }935 fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {936 with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;937 Ok(())938 }939 pub fn to_string(&self) -> Result<String> {940 let ty = self.type_of();941 if !matches!(ty, NixType::String) {942 bail!("unexpected type: {ty:?}, expected string");943 }944 let mut str_out = String::new();945 with_default_context(|c, _| unsafe {946 get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())947 })?;948949 Ok(str_out)950 }951 pub fn to_realised_string(&self) -> Result<RealisedString> {952 with_default_context(|c, es| unsafe { string_realise(c, es, self.0, false) })953 .map(RealisedString)954955 // let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };956 // for i in 0..store_paths {957 // let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };958 // nix_raw::store_path_name(store_path, callback, user_data);959 // }960 // dbg!(store_paths);961 // todo!();962 }963964 pub fn has_field(&self, field: &str) -> Result<bool> {965 if !matches!(self.type_of(), NixType::Attrs) {966 bail!("invalid type: expected attrs");967 }968969 let f = init_field_name(field);970 with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })971 }972 // pub fn derivation_path(&self) {973 // nix_raw::real974 // }975 pub fn list_fields(&self) -> Result<Vec<String>> {976 if !matches!(self.type_of(), NixType::Attrs) {977 bail!("invalid type: expected attrs");978 }979980 let len = with_default_context(|c, _| unsafe { get_attrs_size(c, self.0) })?;981 let mut out = Vec::with_capacity(len as usize);982983 for i in 0..len {984 let name =985 with_default_context(|c, es| unsafe { get_attr_name_byidx(c, self.0, es, i) })?;986 let c = unsafe { CStr::from_ptr(name) };987 out.push(c.to_str().expect("nix field names are utf-8").to_owned());988 }989 Ok(out)990 }991 pub fn get_elem(&self, v: usize) -> Result<Self> {992 if !matches!(self.type_of(), NixType::List) {993 bail!("invalid type: expected list");994 }995 let len = with_default_context(|c, _| unsafe { get_list_size(c, self.0) })? as usize;996 if v >= len {997 bail!("oob list get: {v} >= {len}");998 }9991000 with_default_context(|c, es| unsafe { get_list_byidx(c, self.0, es, v as u32) }).map(Self)1001 }1002 pub fn attrs_update(self, other: Value /*, ignore_errors: bool*/) -> Result<Self> {1003 let attrs_update_fn = Self::eval("a: b: a // b")?;10041005 attrs_update_fn1006 .call(self)?1007 .call(other)1008 .context("attrs update")1009 }1010 pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {1011 if !matches!(self.type_of(), NixType::Attrs) {1012 bail!("invalid type: expected attrs");1013 }10141015 name.as_field_name(|name| {1016 with_default_context(|c, es| unsafe {1017 get_attr_byname(c, self.0, es, name.as_ptr().cast())1018 })1019 .map(Self)1020 })1021 .with_context(|| format!("getting field {:?}", name.to_field_name()))1022 }1023 pub fn call(&self, v: Value) -> Result<Self> {1024 let kind = self1025 .functor_kind()1026 .ok_or_else(|| anyhow!("can only call function or functor"))?;10271028 let function = match kind {1029 FunctorKind::Function => self.clone(),1030 FunctorKind::Functor => {1031 let f = self1032 .get_field("__functor")1033 .context("getting functor value")?;1034 assert_eq!(1035 f.type_of(),1036 NixType::Function,1037 "invalid functor encountered"1038 );1039 f1040 }1041 };10421043 let out = Value::new_uninit();1044 with_default_context(|c, es| unsafe { value_call(c, es, function.0, v.0, out.0) })?;10451046 Ok(out)1047 }1048 pub fn eval(v: &str) -> Result<Self> {1049 let s = CString::new(v).expect("expression shouldn't have internal NULs");1050 let out = Self::new_uninit();1051 with_default_context(|c, es| unsafe {1052 expr_eval_from_string(c, es, s.as_ptr(), c"/root".as_ptr(), out.0)1053 })?;1054 Ok(out)1055 }1056 #[instrument(name = "build", skip(self), fields(output))]1057 pub fn build(&self, output: &str) -> Result<Utf8PathBuf> {1058 if !self.is_derivation() {1059 bail!("expected derivation to build")1060 }1061 let output_name = self1062 .get_field("outputName")1063 .context("getting output name field")?1064 .to_string()?;1065 let v = if output_name != output {1066 let out = self.get_field(output).context("getting target output")?;1067 if !out.is_derivation() {1068 bail!("unknown output: {output}");1069 }1070 out1071 } else {1072 self.clone()1073 };10741075 let drv_path = Utf8PathBuf::from(1076 v.get_field("drvPath")1077 .context("getting drvPath")?1078 .to_string()?,1079 );1080 let graph = Arc::new(drv::DrvGraph::resolve(&eval_store(), &drv_path)?);1081 let _guard = logging::register_build_graph(&Span::current(), &graph);10821083 scheduler::build_graph_sync(graph.clone(), vec![output.to_owned()])?;10841085 let s = v.builtin_to_string()?;1086 let rs = s.to_realised_string()?;1087 let out_path = rs.as_str().to_owned();1088 Ok(Utf8PathBuf::from(out_path))1089 }1090 pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {1091 let to_json = Self::eval("builtins.toJSON")?;1092 let s = to_json.call(self.clone())?.to_string()?;1093 Ok(serde_json::from_str(&s)?)1094 }1095 pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {1096 Self::eval(&nixlike::serialize(v)?)1097 }10981099 // Convert to string/evaluate derivations/etc1100 // fn to_string_weak(&self) -> Result<String> {1101 // // TODO: For now, it works exactly like to_string, see the comment for fn force()1102 // self.to_string()1103 // }11041105 fn is_derivation(&self) -> bool {1106 if !matches!(self.type_of(), NixType::Attrs) {1107 return false;1108 }1109 let Some(ty) = self.get_field("type").ok() else {1110 return false;1111 };1112 matches!(ty.to_string().as_deref(), Ok("derivation"))1113 }1114 fn functor_kind(&self) -> Option<FunctorKind> {1115 match self.type_of() {1116 NixType::Attrs => self1117 .has_field("__functor")1118 .expect("has_field shouldn't fail for attrs")1119 .then_some(FunctorKind::Functor),1120 NixType::Function => Some(FunctorKind::Function),1121 _ => None,1122 }1123 }1124 pub fn is_function(&self) -> bool {1125 self.functor_kind().is_some()1126 }1127 pub fn is_null(&self) -> bool {1128 matches!(self.type_of(), NixType::Null)1129 }1130 pub fn is_string(&self) -> bool {1131 matches!(self.type_of(), NixType::String)1132 }1133 pub fn is_attrs(&self) -> bool {1134 matches!(self.type_of(), NixType::Attrs)1135 }1136}11371138impl From<String> for Value {1139 fn from(value: String) -> Self {1140 Value::new_str(&value)1141 }1142}1143impl From<bool> for Value {1144 fn from(value: bool) -> Self {1145 Value::new_bool(value)1146 }1147}1148impl From<&str> for Value {1149 fn from(value: &str) -> Self {1150 Value::new_str(value)1151 }1152}1153impl<T> From<Vec<T>> for Value1154where1155 T: Into<Value>,1156{1157 fn from(value: Vec<T>) -> Self {1158 Value::new_list(value)1159 }1160}11611162impl Clone for Value {1163 fn clone(&self) -> Self {1164 with_default_context(|c, _| unsafe { value_incref(c, self.0) })1165 .expect("value incref should not fail");1166 Self(self.0)1167 }1168}1169impl Drop for Value {1170 fn drop(&mut self) {1171 with_default_context(|c, _| unsafe { value_decref(c, self.0) })1172 .expect("value drop should not fail");1173 }1174}11751176static TOKIO_FOR_NIX: OnceLock<Arc<tokio::runtime::Runtime>> = OnceLock::new();11771178pub fn init_libraries() {1179 unsafe { GC_allow_register_threads() };11801181 let mut ctx = NixContext::new();1182 ctx.run_in_context(|c| unsafe { libutil_init(c) })1183 .expect("util init should not fail");1184 ctx.run_in_context(|c| unsafe { libstore_init(c) })1185 .expect("store init should not fail");1186 ctx.run_in_context(|c| unsafe { libexpr_init(c) })1187 .expect("expr init should not fail");11881189 nix_logging_cxx::apply_tracing_logger();1190}11911192pub fn init_tokio_for_nix(tokio: Arc<tokio::runtime::Runtime>) {1193 TOKIO_FOR_NIX1194 .set(tokio)1195 .expect("tokio for nix should only be initialized once");1196}11971198pub fn await_in_nix<F: Send + 'static>(f: impl Future<Output = F> + Send + 'static) -> F {1199 // It should be possible to do Handle::current(), but some of the planned features don't work well with that1200 let runtime = TOKIO_FOR_NIX1201 .get()1202 .expect("init_tokio_for_nix was not called");1203 std::thread::spawn(move || runtime.block_on(f))1204 .join()1205 .expect("await_in_nix inner thread panicked")1206}12071208unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(1209 user_data: *mut c_void,1210 mut context: *mut c_context,1211 state: *mut nix_raw::EvalState,1212 args: *mut *mut value,1213 ret: *mut value,1214) {1215 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };1216 let args: [&Value; N] = array::from_fn(|i| {1217 let v: &mut Value = unsafe { &mut *args.add(i).cast() };1218 v as &Value1219 });1220 let ctx: &mut NixContext = unsafe { transmute(&mut context) };12211222 let state: &EvalState = unsafe { std::mem::transmute(&state) };12231224 match user_closure(state, args) {1225 Ok(v) => {1226 unsafe { copy_value(context, ret, v.0) };1227 }1228 Err(e) => {1229 ctx.set_err(e);1230 }1231 }1232}12331234type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;12351236pub struct NativeFn(*mut PrimOp);1237impl NativeFn {1238 pub fn new<const N: usize>(1239 name: &'static CStr,1240 doc: &'static CStr,1241 args: [&'static CStr; N],1242 f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,1243 ) -> Self {1244 // Double-boxing to make it thin pointer, as vtable gets outside of first Box1245 let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1246 let f: PrimOpFun = Some(nix_primop_closure_adapter::<N>);1247 let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec();1248 args.push(null());1249 let args = args.as_mut_ptr();1250 let primop = unsafe {1251 alloc_primop(1252 null_mut(),1253 f,1254 N as i32,1255 name.as_ptr(),1256 args,1257 doc.as_ptr(),1258 Box::into_raw(closure).cast(),1259 )1260 };12611262 assert!(!primop.is_null(), "primop allocation should not fail");12631264 Self(primop)1265 }1266 pub fn register(self) {1267 unsafe { register_primop(null_mut(), self.0) };1268 }1269}12701271pub struct StorePath(*mut c_store_path);1272impl StorePath {1273 fn as_ptr(&self) -> *mut c_store_path {1274 self.01275 }1276}12771278impl Drop for StorePath {1279 fn drop(&mut self) {1280 unsafe { store_path_free(self.0) }1281 }1282}12831284#[test_log::test]1285fn test_native() -> Result<()> {1286 init_libraries();1287 NativeFn::new(1288 c"__uppercaseSuffix2",1289 c"make string uppercase and add suffix",1290 [c"str", c"suffix"],1291 |_, [str, suffix]: [&Value; 2]| {1292 let str = str.to_string()?;1293 let suffix = suffix.to_string()?;1294 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1295 },1296 )1297 .register();12981299 let mut fetch_settings = FetchSettings::new();1300 fetch_settings.set(c"warn-dirty", c"false");13011302 let manifest = format!("git+file://{}/../../", env!("CARGO_MANIFEST_DIR"));1303 let flake = FlakeSettings::new()?;1304 let parse = FlakeReferenceParseFlags::new(&flake)?;1305 let (mut r, _) = FlakeReference::new(&manifest, &flake, &parse, &fetch_settings)?;1306 let lock = FlakeLockFlags::new(&flake)?;1307 let locked = r.lock(&fetch_settings, &flake, &lock)?;1308 let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;13091310 let builtins = Value::eval("builtins")?;1311 assert_eq!(builtins.type_of(), NixType::Attrs);13121313 assert_eq!(attrs.type_of(), NixType::Attrs);1314 let test_data = nix_go!(attrs.testData);13151316 let test_string: String = nix_go_json!(test_data.testString);1317 assert_eq!(test_string, "hello");13181319 let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath);1320 let s = CString::new(s.to_string()?).expect("path str is cstring");13211322 let uppercase_suffix = Value::new_primop(NativeFn::new(1323 c"uppercase_suffix",1324 c"make string uppercase and add suffix",1325 [c"str", c"suffix"],1326 |es, [str, suffix]: [&Value; 2]| {1327 let str = str.to_string()?;1328 let suffix = suffix.to_string()?;1329 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1330 },1331 ));13321333 let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix));1334 assert_eq!(test_result, "PREFIX_BODY_SUFFIX");1335 let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));1336 assert_eq!(test_result, "TESTsuffix");13371338 let drv_path =1339 nix_go!(attrs.packages["x86_64-linux"]["fleet-install-secrets"].drvPath).to_string()?;1340 let graph = drv::DrvGraph::resolve(&drv_path)?;1341 eprintln!(1342 "fleet-install-secrets dependency graph: {} nodes",1343 graph.nodes.len()1344 );1345 for (path, node) in &graph.nodes {1346 if !node.input_drvs.is_empty() {1347 eprintln!(" {} ({} deps)", node.name, node.input_drvs.len());1348 }1349 }13501351 Ok(())1352}13531354// pub struct GcAlloc;1355// unsafe impl GlobalAlloc for GcAlloc {1356// unsafe fn alloc(&self, l: Layout) -> *mut u8 {1357// let ptr = unsafe { GC_malloc(l.size()) };1358// ptr.cast()1359// }1360// unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) {1361// // unsafe { GC_free(ptr.cast()) };1362// }1363//1364// unsafe fn realloc(&self, ptr: *mut u8, _: Layout, new_size: usize) -> *mut u8 {1365// let ptr = unsafe { GC_realloc(ptr.cast(), new_size) };1366// ptr.cast()1367// }1368// }1369//1370// #[global_allocator]1371// static GC: GcAlloc = GcAlloc;crates/nix-eval/src/logging.rsdiffbeforeafterboth--- a/crates/nix-eval/src/logging.rs
+++ b/crates/nix-eval/src/logging.rs
@@ -109,6 +109,12 @@
(ActivityType::CopyPaths, []) => {
debug_span!(target: "nix::copy-paths", "copying paths")
}
+ // Umbrella progress activity. Nix emits it at lvlError ("always show"),
+ // which would otherwise become a noisy ERROR `action` span — give it an
+ // explicit debug level like the other container activities.
+ (ActivityType::Builds, []) => {
+ debug_span!(target: "nix::builds", "building")
+ }
(ActivityType::Unknown, [])
if s.starts_with("copying \"") && s.ends_with("\" to the store") =>
{
@@ -365,6 +371,16 @@
BuildGraphGuard { paths }
}
+/// Drop the lazily-created `building` span for a derivation. Used when a build
+/// fails: its dependents will never build, so their speculatively-created spans
+/// (made when a dependency started building) would otherwise linger until the
+/// whole build graph is torn down.
+pub fn remove_drv_span(drv_path: &Utf8Path) {
+ if let Some(entry) = DRV_GRAPH.lock().expect("not poisoned").get_mut(drv_path) {
+ entry.span = None;
+ }
+}
+
fn ensure_drv_span(drv_path: &Utf8Path) -> Option<Span> {
let mut drv_graph = DRV_GRAPH.lock().expect("not poisoned");
@@ -524,7 +540,7 @@
// ResultType::FileLinked => todo!(),
(ResultType::BuildLogLine, [Str(s)]) => {
let s = ansi_filter(s);
- info!("{s}");
+ info!(target: "nix::build", "{s}");
}
// ResultType::UntrustedPath => todo!(),
// ResultType::CorruptedPath => todo!(),
crates/nix-eval/src/scheduler.rsdiffbeforeafterboth--- a/crates/nix-eval/src/scheduler.rs
+++ b/crates/nix-eval/src/scheduler.rs
@@ -45,9 +45,36 @@
},
}
+// Derivations whose names contain any of these are "heavy": memory-hungry builds that already
+// saturate all cores internally (e.g. composable-kernels), so running anything alongside them
+// just thrashes RAM. A heavy build grabs every semaphore permit and runs exclusively.
+const DEFAULT_HEAVY_DRVS: &[&str] = &[
+ "composable-kernel",
+ "composable_kernel",
+ "ghc",
+ "gfortran",
+ "llvm",
+ "clang",
+];
+
+fn heavy_patterns_from_env() -> Vec<String> {
+ let mut pats: Vec<String> = DEFAULT_HEAVY_DRVS.iter().map(|s| (*s).to_owned()).collect();
+ if let Ok(extra) = std::env::var("FLEET_HEAVY_DRVS") {
+ pats.extend(
+ extra
+ .split(',')
+ .map(str::trim)
+ .filter(|s| !s.is_empty())
+ .map(str::to_owned),
+ );
+ }
+ pats
+}
+
pub struct Scheduler {
store: Arc<Store>,
parallelism: usize,
+ heavy_patterns: Vec<String>,
events: broadcast::Sender<BuildEvent>,
}
@@ -58,10 +85,20 @@
Self {
store: eval_store(),
parallelism,
+ heavy_patterns: heavy_patterns_from_env(),
events,
}
}
+ // Permits a drv must hold to run. Heavy drvs take all of them, pausing everything else.
+ fn permits_for(&self, name: &str) -> u32 {
+ if self.heavy_patterns.iter().any(|p| name.contains(p)) {
+ self.parallelism as u32
+ } else {
+ 1
+ }
+ }
+
pub fn subscribe(&self) -> broadcast::Receiver<BuildEvent> {
self.events.subscribe()
}
@@ -139,6 +176,7 @@
.get(&path)
.map(|n| n.name.clone())
.unwrap_or_default();
+ crate::logging::remove_drv_span(&path);
let _ = self.events.send(BuildEvent::DrvCancelled {
drv_path: path.clone(),
name,
@@ -153,8 +191,21 @@
let graph = graph.clone();
let wanted_here = wanted.get(&path).cloned().unwrap_or_default();
let store = self.store.clone();
+ let permits = graph
+ .nodes
+ .get(&path)
+ .map(|n| self.permits_for(&n.name))
+ .unwrap_or(1);
in_flight.push(tokio::spawn(async move {
- let _permit = sem.acquire_owned().await.expect("semaphore not closed");
+ if permits > 1 {
+ debug!(permits, "heavy drv: waiting for exclusive build slot");
+ }
+ // Heavy drvs acquire every permit, so they only start once all in-flight
+ // builds drain and nothing new can slip in (the semaphore is FIFO-fair).
+ let _permit = sem
+ .acquire_many_owned(permits)
+ .await
+ .expect("semaphore not closed");
let node = graph
.nodes
.get(&path)
@@ -224,6 +275,7 @@
propagate_done(&dependents, &mut indeg, &mut ready, &finished);
}
Err(e) => {
+ crate::logging::remove_drv_span(&finished);
failed.insert(finished.clone(), format!("{e:#}"));
mark_tainted(&dependents, &finished, &mut tainted);
propagate_done(&dependents, &mut indeg, &mut ready, &finished);
@@ -368,11 +420,23 @@
// TODO: Parallelism as a metric works poorly with multiple machines, but I haven't thought about bringing
// hercy here yet. In case of remote machines - they will handle parallelism on their own, and this one
// will work as a hard cap.
+/// Number of derivations to build concurrently, taken from Nix's `max-jobs`
+/// setting. `auto` resolves to the CPU count, matching Nix itself.
+fn max_jobs() -> usize {
+ let cores = || {
+ std::thread::available_parallelism()
+ .map(|p| p.get())
+ .unwrap_or(4)
+ };
+ match crate::get_setting(c"max-jobs") {
+ Ok(v) if v.trim() == "auto" => cores(),
+ Ok(v) => v.trim().parse().unwrap_or_else(|_| cores()),
+ Err(_) => cores(),
+ }
+}
+
pub fn build_graph_sync(graph: Arc<DrvGraph>, root_outputs: Vec<String>) -> Result<()> {
- let parallelism = std::thread::available_parallelism()
- .map(|p| p.get())
- .unwrap_or(4);
- let scheduler = Scheduler::new(parallelism);
+ let scheduler = Scheduler::new(max_jobs());
crate::await_in_nix(async move { scheduler.run(graph, root_outputs).await })
.context("scheduler run")
}
crates/remowt-fleet/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-fleet/src/lib.rs
+++ b/crates/remowt-fleet/src/lib.rs
@@ -28,6 +28,8 @@
Sign(String),
#[error("listing generations failed: {0}")]
ListGenerations(String),
+ #[error("substitution failed: {0}")]
+ Substitute(String),
}
#[endpoints(ns = 91)]
@@ -60,6 +62,20 @@
.map_err(|e| NixError::Sign(e.to_string()))
}
+ /// Download the given paths (and their closures) into the local store using
+ /// the configured substituters. Used to fetch closures that were uploaded to
+ /// a binary cache (e.g. attic) instead of copied over the nix daemon tunnel.
+ #[endpoints(id = 6)]
+ async fn substitute(&self, paths: Vec<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, NixError> {
+ spawn_blocking(move || {
+ let store = eval_store();
+ store.substitute_paths(&paths)
+ })
+ .await
+ .expect("substitution panicked")
+ .map_err(|e| NixError::Substitute(e.to_string()))
+ }
+
#[endpoints(id = 5)]
async fn list_generations(
&self,
modules/cache.nixdiffbeforeafterboth--- /dev/null
+++ b/modules/cache.nix
@@ -0,0 +1,61 @@
+{
+ lib,
+ fleetLib,
+ config,
+ ...
+}:
+let
+ inherit (lib.options) mkOption mkEnableOption literalExpression;
+ inherit (lib.types)
+ enum
+ str
+ bool
+ submodule
+ ;
+ inherit (fleetLib.options) mkHostsOption;
+
+ _file = ./cache.nix;
+
+ cacheOptions = {
+ enable = mkEnableOption "fleet binary cache";
+ kind = mkOption {
+ description = ''
+ Kind of the binary cache to use.
+ '';
+ type = enum [ "attic" ];
+ default = "attic";
+ };
+ name = mkOption {
+ description = ''
+ Name of the binary cache.
+ '';
+ type = str;
+ };
+ pin = mkOption {
+ description = ''
+ Whether to pin pushed paths in the attic cache, preventing them from
+ being garbage collected.
+
+ Requires compatible fork of attic server+client: https://github.com/zhaofengli/attic/pull/227#issuecomment-4759154796
+ '';
+ type = bool;
+ default = false;
+ };
+ };
+in
+{
+ options = {
+ cache = cacheOptions;
+ hosts = mkHostsOption {
+ inherit _file;
+ options.cache = mkOption {
+ description = ''
+ Binary cache configuration for this host.
+ '';
+ type = submodule { options = cacheOptions; };
+ default = config.cache;
+ defaultText = literalExpression "fleetConfiguration.cache";
+ };
+ };
+ };
+}
modules/module-list.nixdiffbeforeafterboth--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -1,5 +1,6 @@
[
./assertions.nix
+ ./cache.nix
./fleetLib.nix
./hosts.nix
./nixos.nix
pkgs/default.nixdiffbeforeafterboth--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -12,8 +12,6 @@
fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; };
inherit remowt-agents-bundle;
- remowt-plugin-fleet = callPackage ./remowt-plugin-fleet.nix {
- inherit craneLib inputs remowt-agents-bundle;
- };
+ remowt-plugin-fleet = callPackage ./remowt-plugin-fleet.nix { inherit craneLib inputs; };
remowt-ssh = callPackage ./remowt-ssh.nix { inherit craneLib remowt-agents-bundle; };
}
remowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- a/remowt/cmds/remowt-agent/src/main.rs
+++ b/remowt/cmds/remowt-agent/src/main.rs
@@ -273,10 +273,11 @@
let helper = SocketHelper {
fallback: SuidHelper,
};
- register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;
+ let rofi = RofiPrompter::default();
+ register_auth_agent(&system_conn, Agent::new(helper, rofi.clone())).await?;
let mut rpc = Rpc::<BifConfig>::new(Address::User);
- serve_prompts(&mut rpc, RofiPrompter);
+ serve_prompts(&mut rpc, rofi);
gateway::serve(rpc.clone(), &gateway::local_socket()?).await?;
remowt/cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth--- a/remowt/cmds/remowt-ssh/Cargo.toml
+++ b/remowt/cmds/remowt-ssh/Cargo.toml
@@ -8,6 +8,7 @@
[dependencies]
clap = { workspace = true, features = ["derive"] }
tracing-subscriber.workspace = true
+tracing-journald.workspace = true
remowt-link-shared.workspace = true
remowt-client.workspace = true
tokio = { workspace = true, features = [
remowt/cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- a/remowt/cmds/remowt-ssh/src/main.rs
+++ b/remowt/cmds/remowt-ssh/src/main.rs
@@ -20,6 +20,8 @@
use tokio::io::{AsyncRead, ReadBuf};
use tokio::signal::unix::{signal, SignalKind};
use tracing::debug;
+use tracing_subscriber::prelude::*;
+use tracing_subscriber::EnvFilter;
#[derive(Parser)]
enum Opts {
@@ -45,10 +47,16 @@
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
- tracing_subscriber::fmt()
- .with_writer(std::io::stderr)
- .without_time()
- .init();
+ // Log to the journal, not stderr: this is an interactive client that puts
+ // the terminal in raw mode, so anything on stderr corrupts the session
+ // output. If the journal socket is unavailable, drop logs rather than fall
+ // back to stderr.
+ if let Ok(journald) = tracing_journald::layer() {
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(journald.with_syslog_identifier("remowt-ssh".to_owned()))
+ .init();
+ }
let opts = Opts::parse();
let bundle = AgentBundle::from_dir(agents_dir()?)?;
remowt/crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth--- a/remowt/crates/remowt-ui-prompt/Cargo.toml
+++ b/remowt/crates/remowt-ui-prompt/Cargo.toml
@@ -12,5 +12,5 @@
remowt-link-shared.workspace = true
serde.workspace = true
thiserror.workspace = true
-tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt", "sync"] }
tracing.workspace = true
remowt/crates/remowt-ui-prompt/src/auto.rsdiffbeforeafterboth--- a/remowt/crates/remowt-ui-prompt/src/auto.rs
+++ b/remowt/crates/remowt-ui-prompt/src/auto.rs
@@ -24,7 +24,7 @@
};
Self {
remote,
- fallback: RofiPrompter,
+ fallback: RofiPrompter::default(),
}
}
remowt/crates/remowt-ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/remowt/crates/remowt-ui-prompt/src/rofi.rs
+++ b/remowt/crates/remowt-ui-prompt/src/rofi.rs
@@ -1,13 +1,18 @@
use std::process::Stdio;
+use std::sync::Arc;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
+use tokio::sync::Mutex;
use tracing::trace;
use crate::{Error, Prompter, Result, Source};
-#[derive(Clone)]
-pub struct RofiPrompter;
+#[derive(Clone, Default)]
+pub struct RofiPrompter {
+ // Rofi can't run concurrently; serialize invocations.
+ lock: Arc<Mutex<()>>,
+}
fn fixup_prompt(prompt: &str) -> &str {
// Rofi always appends such suffix
@@ -27,6 +32,7 @@
source: &[Source],
) -> Result<u32> {
trace!("rofi radio");
+ let _guard = self.lock.lock().await;
let mut cmd = rofi_command();
let mesg = if source.is_empty() {
description.to_owned()
@@ -113,6 +119,7 @@
source: &[Source],
) -> Result<String> {
trace!("rofi text");
+ let _guard = self.lock.lock().await;
let mut cmd = rofi_command();
let mesg = if source.is_empty() {
description.to_owned()
@@ -162,6 +169,7 @@
async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
trace!("rofi display");
+ let _guard = self.lock.lock().await;
let mut cmd = rofi_command();
let mut mesg = if source.is_empty() {
description.to_owned()
@@ -206,7 +214,7 @@
#[ignore = "interactive"]
async fn test() {
let prompter = PrependSourcePrompter {
- prompter: RofiPrompter,
+ prompter: RofiPrompter::default(),
description: "test".to_owned(),
source: vec![Source(Cow::Borrowed("ssh"))],
};