diff --git a/doc/manual/rl-next/summarize-derivation-traces.md b/doc/manual/rl-next/summarize-derivation-traces.md new file mode 100644 index 000000000..ff42ba1e1 --- /dev/null +++ b/doc/manual/rl-next/summarize-derivation-traces.md @@ -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 1 155 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. diff --git a/lix/libexpr/primops.cc b/lix/libexpr/primops.cc index 01007c0db..452929950 100644 --- a/lix/libexpr/primops.cc +++ b/lix/libexpr/primops.cc @@ -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; } } diff --git a/lix/libutil/error.cc b/lix/libutil/error.cc index 93b188ba9..3c1fc2e5a 100644 --- a/lix/libutil/error.cc +++ b/lix/libutil/error.cc @@ -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 #include #include +#include #include @@ -38,6 +41,39 @@ std::ostream & operator <<(std::ostream & os, const HintFmt & hf) return os << hf.str(); } +Trace Trace::fromDrv(std::shared_ptr 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, 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 Range> + requires std::ranges::sized_range +void printDerivationTracesMaybe(std::ostream & oss, Range && elems) +{ + if (std::ranges::empty(elems)) { + return; + } + + std::set> 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(); + printDerivationTracesMaybe(oss, drvTraces); + auto suggestions = einfo.suggestions.trim(); if (!suggestions.suggestions.empty()) { oss << "Did you mean " << diff --git a/lix/libutil/error.hh b/lix/libutil/error.hh index ea8e87f64..9c2ca5ba7 100644 --- a/lix/libutil/error.hh +++ b/lix/libutil/error.hh @@ -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 can't be dereferenced in this header, +// because we can't include position.hh without circular includes. +// Yay. +Trace traceFromDrv(std::shared_ptr pos, std::string drvName); + struct Trace { std::shared_ptr pos; HintFmt hint; + std::optional drvTrace; + + /** Construct a Trace and canned format message assuming a derivation's + * position and name. + */ + static Trace fromDrv(std::shared_ptr 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, std::string drvName, std::string attrOfDrv); }; inline bool operator<(const Trace& lhs, const Trace& rhs); diff --git a/tests/functional2/lang/drv-trace-summary/eval-fail.err.exp b/tests/functional2/lang/drv-trace-summary/eval-fail.err.exp new file mode 100644 index 000000000..3147281fa --- /dev/null +++ b/tests/functional2/lang/drv-trace-summary/eval-fail.err.exp @@ -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' diff --git a/tests/functional2/lang/drv-trace-summary/in.nix b/tests/functional2/lang/drv-trace-summary/in.nix new file mode 100644 index 000000000..ad87a6576 --- /dev/null +++ b/tests/functional2/lang/drv-trace-summary/in.nix @@ -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 diff --git a/tests/functional2/lang/drv-trace-summary/test.toml b/tests/functional2/lang/drv-trace-summary/test.toml new file mode 100644 index 000000000..f67b12be6 --- /dev/null +++ b/tests/functional2/lang/drv-trace-summary/test.toml @@ -0,0 +1,3 @@ +[[test]] +runner = "eval-fail" +global-assets = ["config.nix"]