difftreelog
feat display nix stacktraces
in: trunk
6 files changed
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};23use anyhow::Result;4use clap::Parser;5use fleet_base::{6 deploy::{DeployAction, deploy_task, upload_task},7 host::{Config, DeployKind, GenerationStorage},8 opts::FleetOpts,9};10use nix_eval::nix_go;11use tokio::task::{LocalSet, spawn_blocking};12use tracing::{Instrument, error, field, info, info_span, warn};1314#[derive(Parser)]15pub struct Deploy {16 /// Disable automatic rollback17 #[clap(long)]18 disable_rollback: bool,19 /// Action to execute after system is built20 action: DeployAction,21}2223#[derive(Parser, Clone)]24pub struct BuildSystems {25 /// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes26 /// are "sdImage"/"isoImage", and your configuration may include any other build attributes.27 #[clap(long, default_value = "toplevel")]28 build_attr: String,29}3031async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {32 info!("building");33 let host = config.host(&hostname).await?;34 // let action = Action::from(self.subcommand.clone());35 let nixos = host.nixos_config().await?;36 let drv = nix_go!(nixos.system.build[{ build_attr }]);37 let out_output = spawn_blocking(move || drv.build("out"))38 .await39 .expect("system derivation build should not panic")?;4041 // We already have system profiles for backups.42 if !host.local {43 info!("adding gc root");44 let mut cmd = config.local_host().cmd("nix").await?;45 cmd.arg("build")46 .comparg(47 "--profile",48 format!(49 "/nix/var/nix/profiles/{}-{hostname}",50 config.data().gc_root_prefix51 ),52 )53 .arg(&out_output);54 cmd.sudo().run_nix().await?;55 }5657 Ok(out_output)58}5960impl BuildSystems {61 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {62 let hosts = opts.filter_skipped(config.list_hosts().await?).await?;63 let set = LocalSet::new();64 let build_attr = self.build_attr.clone();65 for host in hosts {66 let config = config.clone();67 let span = info_span!("build", host = field::display(&host.name));68 let hostname = host.name;69 let build_attr = build_attr.clone();70 set.spawn_local(71 (async move {72 let built = match build_task(config, hostname.clone(), &build_attr).await {73 Ok(path) => path,74 Err(e) => {75 error!("failed to deploy host: {}", e);76 return;77 }78 };79 // TODO: Handle error80 let mut out = current_dir().expect("cwd exists");81 out.push(format!("built-{hostname}"));8283 info!("linking iso image to {:?}", out);84 if let Err(e) = symlink(built, out) {85 error!("failed to symlink: {e}")86 }87 })88 .instrument(span),89 );90 }91 set.await;92 Ok(())93 }94}9596impl Deploy {97 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {98 let hosts = opts.filter_skipped(config.list_hosts().await?).await?;99 let set = LocalSet::new();100 for host in hosts.into_iter() {101 let config = config.clone();102 let span = info_span!("deploy", host = field::display(&host.name));103 let hostname = host.name.clone();104 let opts = opts.clone();105 if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {106 host.set_deploy_kind(deploy_kind);107 };108 if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {109 host.set_session_destination(destination);110 };111 if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {112 host.set_legacy_ssh_store(legacy);113 };114115 set.spawn_local(116 (async move {117 let built = match build_task(config.clone(), hostname.clone(), "toplevel").await118 {119 Ok(path) => path,120 Err(e) => {121 error!("failed to build host system closure: {:#}", e);122 return;123 }124 };125126 let deploy_kind = match host.deploy_kind().await {127 Ok(v) => v,128 Err(e) => {129 error!("failed to query target deploy kind: {e}");130 return;131 }132 };133134 // TODO: Make disable_rollback a host attribute instead135 let mut disable_rollback = self.disable_rollback;136 if !disable_rollback && deploy_kind != DeployKind::Fleet {137 warn!("disabling rollback, as not supported by non-fleet deployment kinds");138 disable_rollback = true;139 }140141 let remote_path =142 match upload_task(&config, &host, GenerationStorage::Deployer, built).await143 {144 Ok(v) => v,145 Err(e) => {146 error!("upload failed: {e}");147 return;148 }149 };150151 if let Err(e) = deploy_task(152 self.action,153 &host,154 remote_path,155 match opts.action_attr(&host, "specialisation").await {156 Ok(v) => v,157 _ => {158 error!("unreachable? failed to get specialization");159 return;160 }161 },162 disable_rollback,163 )164 .await165 {166 error!("activation failed: {e}");167 }168 })169 .instrument(span),170 );171 }172 set.await;173 Ok(())174 }175}1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};23use anyhow::Result;4use clap::Parser;5use fleet_base::{6 deploy::{DeployAction, deploy_task, upload_task},7 host::{Config, DeployKind, GenerationStorage},8 opts::FleetOpts,9};10use nix_eval::nix_go;11use tokio::task::{LocalSet, spawn_blocking};12use tracing::{Instrument, error, field, info, info_span, warn};1314#[derive(Parser)]15pub struct Deploy {16 /// Disable automatic rollback17 #[clap(long)]18 disable_rollback: bool,19 /// Action to execute after system is built20 action: DeployAction,21}2223#[derive(Parser, Clone)]24pub struct BuildSystems {25 /// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes26 /// are "sdImage"/"isoImage", and your configuration may include any other build attributes.27 #[clap(long, default_value = "toplevel")]28 build_attr: String,29}3031async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {32 info!("building");33 let host = config.host(&hostname).await?;34 // let action = Action::from(self.subcommand.clone());35 let nixos = host.nixos_config().await?;36 let drv = nix_go!(nixos.system.build[{ build_attr }]);37 let out_output = spawn_blocking(move || drv.build("out"))38 .await39 .expect("system derivation build should not panic")?;4041 // We already have system profiles for backups.42 if !host.local {43 info!("adding gc root");44 let mut cmd = config.local_host().cmd("nix").await?;45 cmd.arg("build")46 .comparg(47 "--profile",48 format!(49 "/nix/var/nix/profiles/{}-{hostname}",50 config.data().gc_root_prefix51 ),52 )53 .arg(&out_output);54 cmd.sudo().run_nix().await?;55 }5657 Ok(out_output)58}5960impl BuildSystems {61 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {62 let hosts = opts.filter_skipped(config.list_hosts().await?).await?;63 let set = LocalSet::new();64 let build_attr = self.build_attr.clone();65 for host in hosts {66 let config = config.clone();67 let span = info_span!("build", host = field::display(&host.name));68 let hostname = host.name;69 let build_attr = build_attr.clone();70 set.spawn_local(71 (async move {72 let built = match build_task(config, hostname.clone(), &build_attr).await {73 Ok(path) => path,74 Err(e) => {75 error!("failed to deploy host: {}", e);76 return;77 }78 };79 // TODO: Handle error80 let mut out = current_dir().expect("cwd exists");81 out.push(format!("built-{hostname}"));8283 info!("linking iso image to {:?}", out);84 if let Err(e) = symlink(built, out) {85 error!("failed to symlink: {e}")86 }87 })88 .instrument(span),89 );90 }91 set.await;92 Ok(())93 }94}9596impl Deploy {97 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {98 let hosts = opts.filter_skipped(config.list_hosts().await?).await?;99 let set = LocalSet::new();100 for host in hosts.into_iter() {101 let config = config.clone();102 let span = info_span!("deploy", host = field::display(&host.name));103 let hostname = host.name.clone();104 let opts = opts.clone();105 if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {106 host.set_deploy_kind(deploy_kind);107 };108 if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {109 host.set_session_destination(destination);110 };111 if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {112 host.set_legacy_ssh_store(legacy);113 };114115 set.spawn_local(116 (async move {117 let built = match build_task(config.clone(), hostname.clone(), "toplevel").await118 {119 Ok(path) => path,120 Err(e) => {121 error!("failed to build host system closure: {:?}", e);122 return;123 }124 };125126 let deploy_kind = match host.deploy_kind().await {127 Ok(v) => v,128 Err(e) => {129 error!("failed to query target deploy kind: {e}");130 return;131 }132 };133134 // TODO: Make disable_rollback a host attribute instead135 let mut disable_rollback = self.disable_rollback;136 if !disable_rollback && deploy_kind != DeployKind::Fleet {137 warn!("disabling rollback, as not supported by non-fleet deployment kinds");138 disable_rollback = true;139 }140141 let remote_path =142 match upload_task(&config, &host, GenerationStorage::Deployer, built).await143 {144 Ok(v) => v,145 Err(e) => {146 error!("upload failed: {e}");147 return;148 }149 };150151 if let Err(e) = deploy_task(152 self.action,153 &host,154 remote_path,155 match opts.action_attr(&host, "specialisation").await {156 Ok(v) => v,157 _ => {158 error!("unreachable? failed to get specialization");159 return;160 }161 },162 disable_rollback,163 )164 .await165 {166 error!("activation failed: {e}");167 }168 })169 .instrument(span),170 );171 }172 set.await;173 Ok(())174 }175}crates/nix-eval/build.rsdiffbeforeafterboth--- a/crates/nix-eval/build.rs
+++ b/crates/nix-eval/build.rs
@@ -16,6 +16,7 @@
// Link nix C++ libraries for cxx
for lib in &[
"nix-util",
+ "nix-util-c",
"nix-store",
"nix-expr",
"nix-flake",
@@ -34,12 +35,12 @@
cxx_build::bridge("src/logging.rs")
.file("src/logging.cc")
- .std("c++20")
+ .std("c++23")
.shared_flag(true)
.compile("nix-eval-logging");
cxx_build::bridge("src/lib.rs")
.file("src/lib.cc")
- .std("c++20")
+ .std("c++23")
.shared_flag(true)
.compile("nix-eval");
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -13,7 +13,7 @@
pub use anyhow::Result;
use tracing::instrument;
-use self::logging::nix_logging_cxx;
+use self::logging::{ErrorInfoBuilder, nix_logging_cxx};
use self::nix_cxx::set_fetcher_setting;
use self::nix_raw::{
BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,
@@ -179,8 +179,9 @@
let code = unsafe { err_code(self.0) };
NixErrorKind::from_int(code)
}
- fn error<'t>(&self) -> Option<Cow<'t, str>> {
+ fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {
if let NixErrorKind::Generic = self.error_kind()? {
+ let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };
let mut err_out = String::new();
unsafe {
err_info_msg(
@@ -190,13 +191,13 @@
(&raw mut err_out).cast(),
)
};
- return Some(Cow::Owned(err_out));
+ return Some((Cow::Owned(err_out), Some(ei)));
};
// TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,
// but it looks ugly
let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };
- Some(unsafe { CStr::from_ptr(str) }.to_string_lossy())
+ Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))
}
fn clean_err(&mut self) {
unsafe {
@@ -205,8 +206,20 @@
}
fn bail_if_error(&self) -> Result<()> {
- if let Some(err) = self.error() {
- bail!("{err}");
+ if let Some((err, stack)) = self.error() {
+ let mut e = Err(anyhow!("{err}"));
+ if let Some(stack) = stack {
+ for ele in stack.stack_frames {
+ e = e.with_context(|| {
+ if ele.pos.is_empty() {
+ ele.msg
+ } else {
+ format!("{} at {}", ele.msg, ele.pos)
+ }
+ })
+ }
+ }
+ return e.context("<nix frames>");
};
Ok(())
}
crates/nix-eval/src/logging.ccdiffbeforeafterboth--- a/crates/nix-eval/src/logging.cc
+++ b/crates/nix-eval/src/logging.cc
@@ -1,9 +1,36 @@
-#include "nix-eval/src/logging.rs"
#include "logging.hh"
#include <nix/util/logging.hh>
+#include <nix/util/position.hh>
using namespace nix;
+rust::Box<ErrorInfoBuilder> copy_error_info(const ErrorInfo &ei) {
+ auto s = ei.msg.str();
+ rust::Slice<const unsigned char> str(
+ reinterpret_cast<const unsigned char *>(s.data()), s.size());
+ auto b = new_error_info(ei.level, str);
+ if (!ei.traces.empty()) {
+ for (auto iter = ei.traces.rbegin(); iter != ei.traces.rend(); ++iter) {
+ auto msg = iter->hint.str();
+
+ rust::Slice<const unsigned char> msgv(
+ reinterpret_cast<const unsigned char *>(msg.data()), msg.size());
+
+ std::ostringstream oss;
+ if (iter->pos) {
+ iter->pos->print(oss, true);
+ }
+ std::string pos = oss.str();
+
+ rust::Slice<const unsigned char> posv(
+ reinterpret_cast<const unsigned char *>(pos.data()), pos.size());
+
+ b->push_stack_frame(msgv, posv);
+ }
+ }
+ return b;
+}
+
struct TracingLogger : Logger {
TracingLogger() {}
@@ -14,10 +41,8 @@
emit_log(lvl, str);
}
void logEI(const ErrorInfo &ei) override {
- auto s = ei.msg.str();
- rust::Slice<const unsigned char> str(
- reinterpret_cast<const unsigned char *>(s.data()), s.size());
- emit_log(ei.level, str);
+ auto b = copy_error_info(ei);
+ b->emit_error_info();
}
void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
@@ -74,4 +99,8 @@
logger = std::make_unique<TracingLogger>();
// verbosity = lvlVomit;
}
+rust::Box<ErrorInfoBuilder>
+extract_error_info(const nix_c_context *read_context) {
+ return copy_error_info(read_context->info.value());
+}
}
crates/nix-eval/src/logging.hhdiffbeforeafterboth--- a/crates/nix-eval/src/logging.hh
+++ b/crates/nix-eval/src/logging.hh
@@ -1,5 +1,12 @@
#pragma once
+#include "nix-eval/src/logging.rs"
+#include "rust/cxx.h"
+#include <nix_api_util.h>
+#include <nix_api_util_internal.h>
+
+struct ErrorInfoBuilder;
extern "C" {
void apply_tracing_logger();
+rust::Box<ErrorInfoBuilder> extract_error_info(const nix_c_context *ctx);
}
crates/nix-eval/src/logging.rsdiffbeforeafterboth--- a/crates/nix-eval/src/logging.rs
+++ b/crates/nix-eval/src/logging.rs
@@ -2,6 +2,7 @@
use std::fmt::Arguments;
use std::sync::{LazyLock, Mutex};
+use cxx::ExternType;
use tracing::{
Level, Span, debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn,
warn_span,
@@ -535,6 +536,45 @@
out.output
}
+#[derive(Debug)]
+pub struct StackFrame {
+ pub msg: String,
+ pub pos: String,
+}
+
+#[derive(Debug)]
+pub struct ErrorInfoBuilder {
+ level: Level,
+ msg: String,
+ pub stack_frames: Vec<StackFrame>,
+}
+fn new_error_info(lvl: u32, v: &[u8]) -> Box<ErrorInfoBuilder> {
+ let verbosity = Verbosity::from_int(lvl);
+ let level: Level = verbosity.into();
+ let v = String::from_utf8_lossy(v);
+ Box::new(ErrorInfoBuilder {
+ level,
+ msg: v.to_string(),
+ stack_frames: Vec::new(),
+ })
+}
+impl ErrorInfoBuilder {
+ fn push_stack_frame(&mut self, v: &[u8], pos: &[u8]) {
+ let v = String::from_utf8_lossy(v);
+ let pos = String::from_utf8_lossy(pos);
+ self.stack_frames.push(StackFrame {
+ msg: v.to_string(),
+ pos: pos.to_string(),
+ });
+ }
+ fn emit_error_info(&mut self) {
+ error!("{}", self.msg);
+ for frame in &self.stack_frames {
+ error!(" {} at {}", frame.msg, frame.pos)
+ }
+ }
+}
+
#[cxx::bridge]
pub mod nix_logging_cxx {
extern "Rust" {
@@ -544,7 +584,14 @@
fn add_string_field(&mut self, v: &[u8]);
fn emit(&mut self, parent: u64, s: &str);
fn emit_result(&mut self, ty: u32);
-
+ }
+ extern "Rust" {
+ type ErrorInfoBuilder;
+ fn new_error_info(lvl: u32, v: &[u8]) -> Box<ErrorInfoBuilder>;
+ fn push_stack_frame(&mut self, v: &[u8], pos: &[u8]);
+ fn emit_error_info(&mut self);
+ }
+ extern "Rust" {
fn emit_warn(v: &str);
fn emit_stop(id: u64);
fn emit_log(lvl: u32, v: &[u8]);
@@ -552,6 +599,15 @@
unsafe extern "C++" {
include!("nix-eval/src/logging.hh");
+ type nix_c_context = crate::nix_raw::c_context;
+
fn apply_tracing_logger();
+ unsafe fn extract_error_info(ctx: *const nix_c_context) -> Box<ErrorInfoBuilder>;
}
}
+
+unsafe impl ExternType for crate::nix_raw::c_context {
+ type Id = cxx::type_id!("nix_c_context");
+
+ type Kind = cxx::kind::Opaque;
+}