A third possibility: add an unsafe builtin that serializes without forcing any more thunks, and use that builtin after evaluating system. The result would be only those configuration values that were actually used in evaluating the system, with no irrelevant attributes and no errors (assuming the system evaluates without errors, of course.)
A proof of concept: I added this tiny bit of code to src/libexpr/primops.cc:
static void prim_unsafeToXMLNoEval(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
std::ostringstream out;
NixStringContext context;
auto arg = *args[0];
state.forceValue(arg, pos);
printValueAsXML(state, false, false, arg, out, context, pos);
v.mkString(toView(out), context);
}
static RegisterPrimOp primop_unsafeToXMLNoEval({
.name = "__unsafeToXMLNoEval",
.args = {"e"},
.doc = R"(
As `toXML`, but without forcing any thunks. The result will depend on how
much of the argument has already been evaluated.
)",
.fun = prim_unsafeToXMLNoEval,
});
Now I can run this to get a relatively diff-friendly XML dump of my config:
build/src/nix/nix eval --raw --impure --expr \
'let inherit (import <nixpkgs/nixos> { }) config system; in builtins.seq system (builtins.unsafeToXMLNoEval config)' \
> config.xml
Finding or writing an XML diffing tool that compares these dumps for a more user-friendly presentation of which attribute paths changed and how is left as an exercise for the reader.