From 929423476e56858d105cd9f19e1a586dc6f15252 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Wed, 28 May 2025 15:55:41 -0700 Subject: [PATCH] WIP: Allow for null packages and formatters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is useful for frameworks (such as flake-parts) to avoid unnecessary evaluation. See discussion here: https://github.com/hercules-ci/flake-parts/issues/288#issuecomment-2912459614 While digging into this, I discovered that `nix fmt` already handles `null` formatters identically to undefined formatters. I added a couple of tests to demonstrate this behavior. `nix flake show` needs some reworking to avoid crashing with a "error: expected a derivation" when it encounters a `null` formatter or package. My changes here avoid the crash, but has some cosmetic issues, which is why I've labeled this PR as a draft. Cosmetic issues =============== With the following `flake.nix`: ```nix { outputs = _: { packages.x86_64-linux.default = null; }; } ``` `nix flake show` shows a weird empty system: ``` $ nix flake show path:/tmp/tmp.uL0iGuNwXB?lastModified=1748472558&narHash=sha256-tC%2BhdXAyoeFvWHNllJbois8X%2B7wpZ6CJzEzbcaGQxtM%3D └───packages └───x86_64-linux ``` Similarly, `nix flake show --json` includes an empty object: ``` $ nix flake show --json { "packages": { "x86_64-linux": { "default": null } } } ``` `nix build` crashes: ``` $ nix build .#default error: expected flake output attribute 'packages.x86_64-linux.default' to be a derivation or path but found null: null ``` --- src/libexpr/eval-cache.cc | 9 ++++++ src/libexpr/include/nix/expr/eval-cache.hh | 2 ++ src/nix/flake.cc | 12 ++++--- tests/functional/flakes/common.sh | 1 + tests/functional/flakes/show.sh | 4 +-- tests/functional/formatter.sh | 37 ++++++++++++++++++++++ 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index 0d732f59c..8850032ba 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -1,3 +1,4 @@ +#include "nix/expr/value.hh" #include "nix/util/users.hh" #include "nix/expr/eval-cache.hh" #include "nix/store/sqlite.hh" @@ -762,6 +763,14 @@ std::vector AttrCursor::getAttrs() return attrs; } +bool AttrCursor::isNull() +{ + // <<< TODO: caching? >>> + auto & v = forceValue(); + + return v.type() == nNull; +} + bool AttrCursor::isDerivation() { auto aType = maybeGetAttr("type"); diff --git a/src/libexpr/include/nix/expr/eval-cache.hh b/src/libexpr/include/nix/expr/eval-cache.hh index 31873f7a3..d6b92e049 100644 --- a/src/libexpr/include/nix/expr/eval-cache.hh +++ b/src/libexpr/include/nix/expr/eval-cache.hh @@ -153,6 +153,8 @@ public: bool isDerivation(); + bool isNull(); + Value & forceValue(); /** diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 95cf85663..4ee85ca8a 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1340,10 +1340,14 @@ struct CmdFlakeShow : FlakeCommand, MixJSON } } else { try { - if (visitor.isDerivation()) - showDerivation(); - else - throw Error("expected a derivation"); + if (visitor.isNull()) { + j = nullptr; + } else{ + if (visitor.isDerivation()) + showDerivation(); + else + throw Error("expected a derivation"); + } } catch (IFDError & e) { if (!json) { logger->cout(fmt("%s " ANSI_WARNING "omitted due to use of import from derivation" ANSI_NORMAL, headerPrefix)); diff --git a/tests/functional/flakes/common.sh b/tests/functional/flakes/common.sh index 422cab96c..b47364a4f 100644 --- a/tests/functional/flakes/common.sh +++ b/tests/functional/flakes/common.sh @@ -16,6 +16,7 @@ writeSimpleFlake() { foo = import ./simple.nix; fooScript = (import ./shell.nix {}).foo; default = foo; + noPackage = null; }; packages.someOtherSystem = rec { foo = import ./simple.nix; diff --git a/tests/functional/flakes/show.sh b/tests/functional/flakes/show.sh index 7fcc6aca9..0e730c64e 100755 --- a/tests/functional/flakes/show.sh +++ b/tests/functional/flakes/show.sh @@ -53,7 +53,7 @@ cat >flake.nix < show-output.json nix eval --impure --expr ' let show_output = builtins.fromJSON (builtins.readFile ./show-output.json); in -assert show_output == { }; +assert show_output == { formatter = { x86_64-linux = null; }; }; true ' diff --git a/tests/functional/formatter.sh b/tests/functional/formatter.sh index 6631dd6b8..5d25c0b6d 100755 --- a/tests/functional/formatter.sh +++ b/tests/functional/formatter.sh @@ -85,3 +85,40 @@ rm ./my-result # Flake outputs check. nix flake check nix flake show | grep -P "package 'formatter'" + +function expectFailWithOutputMatching() { + outputMustMatch=$1 + + if output=$(nix fmt 2>&1); then + echo >&2 "nix fmt unexpectedly succeeded" + exit 1 + fi + + if ! echo "$output" | grep "$outputMustMatch"; then + echo >&2 "Expected nix fmt output to match:" + echo >&2 "$outputMustMatch" + echo >&2 "However, the actual output was:" + echo >&2 "$output" + exit 1 + fi +} + +# Try a flake with no formatter. +cat << EOF > flake.nix +{ + outputs = _: {}; +} +EOF +expectFailWithOutputMatching "does not provide attribute 'formatter.$system'" +# Confirm that a null formatter is treated as if there is no formatter. +cat << EOF > flake.nix +{ + outputs = _: { + formatter.$system = null; + }; +} +EOF +if nix fmt | grep "does not provide attribute 'formatter.$system'"; then + echo >&2 "nix fmt unexpectedly succeeded" + exit 1 +fi