libexpr/libutil: summarize derivations in stack traces regardless of --show-trace

Eval errors will now print a simple, no frills chain of involved
derivations at the bottom. For example, trying to evaluate
`pkgs.xonsh.override { python3 = pkgs.python2; }` has the usual

Package ‘python-2.7.18.8’ in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/development/interpreters/python/cpython/2.7/default.nix:398 is marked as insecure, refusing to evaluate.

message, but now also includes the following:

       note: trace involved the following derivations:
       derivation 'xonsh-0.19.9'
       derivation 'python2.7-xonsh-0.19.9'
       derivation 'python2.7-setuptools-44.0.0'

To give the user information about why the erroring derivation was
involved in the first place.

We would like more structured information in the future, but this should
still be a significant improvement.

Change-Id: Icf6da52abd0a043cfb63943bf0b0c160c21ee59e
This commit is contained in:
Qyriad
2025-10-27 20:28:27 +01:00
parent 73c019c57b
commit 9523446901
7 changed files with 293 additions and 6 deletions
@@ -0,0 +1,72 @@
---
synopsis: Stack traces now summarize involved derivations at the bottom
cls: [4493]
category: Improvements
credits: [Qyriad]
---
When evaluation errors and a stack trace is printed,
For example, if I add Nheko to a NixOS `environment.systemPackages` without adding `olm-3.2.16` `nixpkgs.config.permittedInsecurePackages`, then without `--show-trace`, I previously got this:
```
error:
… while calling the 'head' builtin
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/attrsets.nix:1701:13:
1700| if length values == 1 || pred here (elemAt values 1) (head values) then
1701| head values
| ^
1702| else
… while evaluating the attribute 'value'
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/modules.nix:1118:7:
1117| // {
1118| value = addErrorContext "while evaluating the option `${showOption loc}':" value;
| ^
1119| inherit (res.defsFinal') highestPrio;
(stack trace truncated; use '--show-trace' to show the full trace)
error: Package olm-3.2.16 in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/by-name/ol/olm/package.nix:37 is marked as insecure, refusing to evaluate.
< -snip the whole explanation about olm's CVEs- >
```
This doesn't tell me anything about where `olm-3.2.16` came from.
With `--show-trace`, there's 1155 lines to sift through, but does contain lines like "while evaluating derivation 'nheko-0.12.1'".
With this change, those lines are summarized and collected at the bottom, regardless of `--show-trace`:
```
error:
… while calling the 'head' builtin
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/attrsets.nix:1701:13:
1700| if length values == 1 || pred here (elemAt values 1) (head values) then
1701| head values
| ^
1702| else
… while evaluating the attribute 'value'
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/modules.nix:1118:7:
1117| // {
1118| value = addErrorContext "while evaluating the option `${showOption loc}':" value;
| ^
1119| inherit (res.defsFinal') highestPrio;
(stack trace truncated; use '--show-trace' to show the full trace)
error: Package olm-3.2.16 in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/by-name/ol/olm/package.nix:37 is marked as insecure, refusing to evaluate.
< -snip the whole explanation about olm's CVEs- >
note: trace involved the following derivations:
derivation 'etc'
derivation 'dbus-1'
derivation 'system-path'
derivation 'nheko-0.12.1'
derivation 'mtxclient-0.10.1'
```
Now we finally know that olm was evaluated because of Nheko, without sifting through *thousands* of lines of error message.
+11 -6
View File
@@ -853,10 +853,12 @@ static void prim_derivationStrict(EvalState & state, Value * * args, Value & v)
* often results from the composition of several functions
* (derivationStrict, derivation, mkDerivation, mkPythonModule, etc.)
*/
e.addTrace(nullptr, HintFmt(
"while evaluating derivation '%s'\n"
" whose name attribute is located at %s",
drvName, pos));
e.pushTrace(Trace::fromDrv(
state.ctx.positions[nameAttr->pos],
drvName
));
throw;
}
}
@@ -1082,8 +1084,11 @@ drvName, Bindings * attrs, Value & v)
}
} catch (Error & e) {
e.addTrace(state.ctx.positions[i->pos],
HintFmt("while evaluating attribute '%1%' of derivation '%2%'", key, drvName));
e.pushTrace(Trace::fromDrvAttr(
state.ctx.positions[i->pos],
std::string(drvName),
std::string(key)
));
throw;
}
}
+66
View File
@@ -1,3 +1,4 @@
#include "lix/libutil/concepts.hh"
#include "lix/libutil/environment-variables.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/logging.hh"
@@ -5,10 +6,12 @@
#include "lix/libutil/terminal.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/signals.hh"
#include "lix/libutil/position.hh"
#include <iostream>
#include <optional>
#include <sstream>
#include <ranges>
#include <boost/core/demangle.hpp>
@@ -38,6 +41,39 @@ std::ostream & operator <<(std::ostream & os, const HintFmt & hf)
return os << hf.str();
}
Trace Trace::fromDrv(std::shared_ptr<Pos> pos, std::string drvName)
{
DrvTrace dt(drvName);
HintFmt h(
"while evaluating derivation '%s'\n"
" whose name attribute is located at %s",
dt.drvName,
*pos
);
return Trace{
.pos = pos,
.hint = h,
.drvTrace = dt,
};
}
Trace Trace::fromDrvAttr(std::shared_ptr<Pos> pos, std::string drvName, std::string attrOfDrv)
{
DrvTrace dt(drvName);
HintFmt h("while evaluating attribute '%s' of derivation '%s'", attrOfDrv, dt.drvName);
return Trace{
.pos = pos,
.hint = h,
.drvTrace = dt,
};
}
/**
* An arbitrarily defined value comparison for the purpose of using traces in the key of a sorted container.
*/
@@ -222,6 +258,29 @@ void printSkippedTracesMaybe(
skippedTraces.clear();
}
template<ViewOf<DrvTrace const &> Range>
requires std::ranges::sized_range<Range>
void printDerivationTracesMaybe(std::ostream & oss, Range && elems)
{
if (std::ranges::empty(elems)) {
return;
}
std::set<std::reference_wrapper<DrvTrace const>> seen;
oss << "\n"
<< ANSI_BLUE << "note:" << ANSI_NORMAL << " trace involved the following derivations: \n";
for (DrvTrace const & dt : elems) {
if (!seen.contains(dt)) {
oss << HintFmt("derivation '%s'", dt.drvName) << "\n";
}
seen.insert(dt);
}
oss << "\n";
}
std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool showTrace)
{
std::string prefix;
@@ -401,6 +460,13 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
printPosMaybe(oss, "", einfo.pos);
// Summarize derivations involved in the trace.
auto const drvTraces = einfo.traces
| std::views::filter([](auto const & trace) { return trace.drvTrace.has_value(); })
| std::views::transform([](auto const & trace) { return *trace.drvTrace; })
| std::ranges::to<std::vector>();
printDerivationTracesMaybe(oss, drvTraces);
auto suggestions = einfo.suggestions.trim();
if (!suggestions.suggestions.empty()) {
oss << "Did you mean " <<
+36
View File
@@ -67,9 +67,45 @@ void printCodeLines(std::ostream & out,
const Pos & errPos,
const LinesOfCode & loc);
/** @brief Information for a @ref Trace that encountered a derivation.
*
* This is used for summarizing the derivations involved in an eval error
* at the end of a trace-print.
*/
struct DrvTrace
{
std::string drvName;
// TODO: include more structured information like "element 6 of nativeBuildInputs".
DrvTrace() = delete;
explicit DrvTrace(std::string drvName) : drvName(drvName) {}
friend std::strong_ordering operator<=>(DrvTrace const & lhs, DrvTrace const & rhs) noexcept = default;
};
struct Trace;
// n.b.: std::shared_ptr<Pos> can't be dereferenced in this header,
// because we can't include position.hh without circular includes.
// Yay.
Trace traceFromDrv(std::shared_ptr<Pos> pos, std::string drvName);
struct Trace {
std::shared_ptr<Pos> pos;
HintFmt hint;
std::optional<DrvTrace> drvTrace;
/** Construct a Trace and canned format message assuming a derivation's
* position and name.
*/
static Trace fromDrv(std::shared_ptr<Pos> pos, std::string drvName);
/** Construct a Trace and canned format message assuming a derivation's
* position, name, and the attribute of that derivation which caused the
* trace.
*/
static Trace fromDrvAttr(std::shared_ptr<Pos> pos, std::string drvName, std::string attrOfDrv);
};
inline bool operator<(const Trace& lhs, const Trace& rhs);
@@ -0,0 +1,75 @@
error:
… while calling the 'getAttr' builtin
at «internal»:1:500:
… while calling the 'derivationStrict' builtin
at «internal»:1:208:
… while evaluating derivation 'package-you-care-about'
whose name attribute is located at /pwd/in.nix:22:5
at /pwd/in.nix:22:5:
21| package-you-care-about = derivation {
22| name = "package-you-care-about";
| ^
23| __structuredAttrs = true;
… while evaluating attribute 'buildInputs' of derivation 'package-you-care-about'
at /pwd/in.nix:25:5:
24|
25| buildInputs = [
| ^
26| direct-dependency
… while evaluating list element at index 0
… while calling the 'getAttr' builtin
at «internal»:1:500:
… while calling the 'derivationStrict' builtin
at «internal»:1:208:
… while evaluating derivation 'direct-dependency'
whose name attribute is located at /pwd/in.nix:15:5
at /pwd/in.nix:15:5:
14| direct-dependency = derivation {
15| name = "direct-dependency";
| ^
16| __structuredAttrs = true;
… while evaluating attribute 'libtrans' of derivation 'direct-dependency'
at /pwd/in.nix:18:5:
17|
18| libtrans = transitive-dependency;
| ^
19| };
… while calling the 'getAttr' builtin
at «internal»:1:500:
… while calling the 'derivationStrict' builtin
at «internal»:1:208:
… while evaluating derivation 'transitive-dependency'
whose name attribute is located at /pwd/in.nix:4:5
at /pwd/in.nix:4:5:
3| transitive-dependency = mkDerivation {
4| name = "transitive-dependency";
| ^
5| __structuredAttrs = true;
… while evaluating attribute 'nativeBuildInputs' of derivation 'transitive-dependency'
at /pwd/in.nix:7:5:
6|
7| nativeBuildInputs = [
| ^
8| null
… while evaluating list element at index 1
… caused by explicit throw
at /pwd/in.nix:9:8:
8| null
9| (throw "transitive dependency growls")
| ^
10| ];
error: transitive dependency growls
note: trace involved the following derivations:
derivation 'package-you-care-about'
derivation 'direct-dependency'
derivation 'transitive-dependency'
@@ -0,0 +1,30 @@
with import ./config.nix;
let
transitive-dependency = mkDerivation {
name = "transitive-dependency";
__structuredAttrs = true;
nativeBuildInputs = [
null
(throw "transitive dependency growls")
];
};
direct-dependency = derivation {
name = "direct-dependency";
__structuredAttrs = true;
libtrans = transitive-dependency;
};
package-you-care-about = derivation {
name = "package-you-care-about";
__structuredAttrs = true;
buildInputs = [
direct-dependency
];
};
in package-you-care-about.outPath
@@ -0,0 +1,3 @@
[[test]]
runner = "eval-fail"
global-assets = ["config.nix"]