Skip to content

Commit 11d8955

Browse files
domenkozarclaude
andcommitted
libcmd: honor AutoCall for flake installables
`InstallableFlake::getCursors()` ignored its `AutoCall` argument, so a flake output that is a function was handed to `nix run`, `nix search`, `nix bundle` and `nix repl` uncalled, and they failed on it instead of applying it to its default arguments. Auto-call the cursor when the caller asks for it. The auto-called value generally differs from the one at that attribute path, so it gets its own slot in the evaluation cache, recorded as a `<auto-call>` pseudo-attribute of the cursor. Sharing the attribute's own key would let a later `AutoCall::No` lookup pick up the auto-called result rather than failing on the function. The call itself happens lazily, when the cursor's value is first needed, so cached attributes are still served without evaluating anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Curu4eZVEzFJZnA7ZADPz
1 parent 973e59b commit 11d8955

7 files changed

Lines changed: 117 additions & 8 deletions

File tree

src/libcmd/installable-flake.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ std::pair<Value *, PosIdx> InstallableFlake::toValue(EvalState & state, AutoCall
146146
return {&getCursor(state, autoCall)->forceValue(), noPos};
147147
}
148148

149-
std::vector<ref<eval_cache::AttrCursor>> InstallableFlake::getCursors(EvalState & state, AutoCall)
149+
std::vector<ref<eval_cache::AttrCursor>> InstallableFlake::getCursors(EvalState & state, AutoCall autoCall)
150150
{
151151
auto evalCache = openEvalCache(state, getLockedFlake());
152152

@@ -163,7 +163,7 @@ std::vector<ref<eval_cache::AttrCursor>> InstallableFlake::getCursors(EvalState
163163
try {
164164
auto attr = root->findAlongAttrPath(AttrPath::parse(state, attrPath));
165165
if (attr) {
166-
res.push_back(ref(*attr));
166+
res.push_back(autoCall == AutoCall::Yes ? (*attr)->autoCall() : ref(*attr));
167167
} else {
168168
suggestions += attr.getSuggestions();
169169
}

src/libexpr/eval-cache.cc

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,10 +374,15 @@ ref<AttrCursor> EvalCache::getRoot()
374374
}
375375

376376
AttrCursor::AttrCursor(
377-
ref<EvalCache> root, Parent parent, Value * value, std::optional<std::pair<AttrId, AttrValue>> && cachedValue)
377+
ref<EvalCache> root,
378+
Parent parent,
379+
Value * value,
380+
std::optional<std::pair<AttrId, AttrValue>> && cachedValue,
381+
bool autoCalled)
378382
: root(root)
379383
, parent(parent)
380384
, cachedValue(std::move(cachedValue))
385+
, autoCalled(autoCalled)
381386
{
382387
if (value)
383388
_value = allocRootValue(value);
@@ -397,7 +402,12 @@ AttrKey AttrCursor::getKey()
397402
Value & AttrCursor::getValue()
398403
{
399404
if (!_value) {
400-
if (parent) {
405+
if (autoCalled) {
406+
assert(parent);
407+
auto * result = root->state.allocValue();
408+
root->state.autoCallFunction(Bindings::emptyBindings, parent->first->forceValue(), *result);
409+
_value = allocRootValue(result);
410+
} else if (parent) {
401411
auto & vParent = parent->first->getValue();
402412
root->state.forceAttrs(vParent, noPos, "while searching for an attribute");
403413
auto attr = vParent.attrs()->get(parent->second);
@@ -422,7 +432,10 @@ AttrPath AttrCursor::getAttrPath() const
422432
{
423433
if (parent) {
424434
auto attrPath = parent->first->getAttrPath();
425-
attrPath.push_back(parent->second);
435+
/* The auto-call slot is an implementation detail, so it doesn't
436+
show up in the user-visible attribute path. */
437+
if (!autoCalled)
438+
attrPath.push_back(parent->second);
426439
return attrPath;
427440
} else
428441
return {};
@@ -455,7 +468,10 @@ Value & AttrCursor::forceValue()
455468
root->state.forceValue(v, noPos);
456469
} catch (EvalError &) {
457470
debug("setting '%s' to failed", getAttrPathStr());
458-
if (root->db)
471+
/* An auto-call is not an attribute of its parent, so
472+
`CachedEvalError::force()` could not reproduce the original
473+
error from a cached failure. Just don't cache it. */
474+
if (root->db && !autoCalled)
459475
cachedValue = {root->db->setFailed(getKey()), failed_t()};
460476
throw;
461477
}
@@ -757,6 +773,31 @@ bool AttrCursor::isDerivation()
757773
return aType && aType->getString() == "derivation";
758774
}
759775

776+
ref<AttrCursor> AttrCursor::autoCall()
777+
{
778+
/* The auto-called value generally differs from the one at this
779+
attribute path, so it gets its own slot in the evaluation cache,
780+
recorded as a pseudo-attribute of this cursor. (A flake output
781+
actually named `<auto-call>` would collide, but only if it is a
782+
function, in which case it has no attributes of its own anyway.) */
783+
auto name = root->state.symbols.create("<auto-call>");
784+
785+
std::optional<std::pair<AttrId, AttrValue>> cachedValue2;
786+
787+
if (root->db) {
788+
fetchCachedValue();
789+
if (!cachedValue)
790+
cachedValue = {root->db->setPlaceholder(getKey()), placeholder_t()};
791+
AttrKey key{cachedValue->first, name};
792+
cachedValue2 = root->db->getAttr(key);
793+
if (!cachedValue2)
794+
cachedValue2 = {root->db->setPlaceholder(key), placeholder_t()};
795+
}
796+
797+
return make_ref<AttrCursor>(
798+
root, std::make_pair(ref(shared_from_this()), name), nullptr, std::move(cachedValue2), true);
799+
}
800+
760801
StorePath AttrCursor::forceDerivation()
761802
{
762803
auto aDrvPath = getAttr(root->state.s.drvPath);

src/libexpr/include/nix/expr/eval-cache.hh

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ class AttrCursor : public std::enable_shared_from_this<AttrCursor>
108108
RootValue _value;
109109
std::optional<std::pair<AttrId, AttrValue>> cachedValue;
110110

111+
/**
112+
* Whether this cursor holds the result of auto-calling its parent
113+
* (see `autoCall()`) rather than one of its parent's attributes.
114+
*/
115+
bool autoCalled = false;
116+
111117
AttrKey getKey();
112118

113119
Value & getValue();
@@ -126,7 +132,8 @@ public:
126132
ref<EvalCache> root,
127133
Parent parent,
128134
Value * value = nullptr,
129-
std::optional<std::pair<AttrId, AttrValue>> && cachedValue = {});
135+
std::optional<std::pair<AttrId, AttrValue>> && cachedValue = {},
136+
bool autoCalled = false);
130137

131138
AttrPath getAttrPath() const;
132139

@@ -168,6 +175,18 @@ public:
168175

169176
Value & forceValue();
170177

178+
/**
179+
* Return a cursor for this value, auto-called with no automatic
180+
* arguments if it is a function (see `EvalState::autoCallFunction()`).
181+
* The returned cursor keeps this cursor's attribute path, but gets its
182+
* own slot in the evaluation cache, since auto-calling generally
183+
* yields a value other than the one at that path.
184+
*
185+
* The call happens lazily, when the cursor's value is first needed, so
186+
* cached attributes are still served without evaluating anything.
187+
*/
188+
ref<AttrCursor> autoCall();
189+
171190
/**
172191
* Force creation of the .drv file in the Nix store.
173192
*/

tests/functional/flakes/bundle.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ cat <<EOF > flake.nix
1717
default = simple;
1818
};
1919
packages.$system.default = import ./simple.nix;
20+
packages.$system.functionPackage = {}: import ./simple.nix;
2021
apps.$system.default = {
2122
type = "app";
2223
program = "\${import ./simple.nix}/hello";
@@ -29,6 +30,7 @@ nix build .#
2930
nix bundle --bundler .# .#
3031
nix bundle --bundler .#bundlers."$system".default .#packages."$system".default
3132
nix bundle --bundler .#bundlers."$system".simple .#packages."$system".default
33+
nix bundle --bundler .#bundlers."$system".simple .#packages."$system".functionPackage
3234

3335
nix bundle --bundler .#bundlers."$system".default .#apps."$system".default
3436
nix bundle --bundler .#bundlers."$system".simple .#apps."$system".default

tests/functional/flakes/eval-cache.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,33 @@ nix build --no-link "$flake1Dir#stack-depth"
4848
expect 1 nix build "$flake1Dir#ifd" --option allow-import-from-derivation false 2>&1 \
4949
| grepQuiet 'error: cannot build .* during evaluation because the option '\''allow-import-from-derivation'\'' is disabled'
5050
nix build --no-link "$flake1Dir#ifd"
51+
52+
# Commands that auto-call the installable ('nix search', 'nix run', ...) must
53+
# still use the cache: the trace fires while the cache is cold, and never again.
54+
flake2Dir="$TEST_ROOT/eval-cache-auto-call-flake"
55+
56+
createGitRepo "$flake2Dir" ""
57+
cp ../simple.nix ../simple.builder.sh "${config_nix}" "$flake2Dir/"
58+
git -C "$flake2Dir" add simple.nix simple.builder.sh config.nix
59+
60+
cat >"$flake2Dir/flake.nix" <<EOF
61+
{
62+
outputs = { self }: let inherit (import ./config.nix) mkDerivation; in {
63+
legacyPackages.$system = {};
64+
packages.$system = builtins.trace "evaluating packages" {
65+
cached = mkDerivation {
66+
name = "cached";
67+
buildCommand = ''
68+
echo true > \$out
69+
'';
70+
};
71+
};
72+
};
73+
}
74+
EOF
75+
76+
git -C "$flake2Dir" add flake.nix
77+
git -C "$flake2Dir" commit -m "Init"
78+
79+
nix search --no-write-lock-file "$flake2Dir" ^ 2>&1 | grepQuiet "evaluating packages"
80+
nix search --no-write-lock-file "$flake2Dir" ^ 2>&1 | grepQuietInverse "evaluating packages"

tests/functional/flakes/run.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ cat <<EOF > flake.nix
1818
type = "app";
1919
program = "\${(import ./shell-hello.nix).hello}/bin/hello";
2020
};
21+
apps.$system.functionApp = {}: {
22+
type = "app";
23+
program = "\${(import ./shell-hello.nix).hello}/bin/hello";
24+
};
2125
};
2226
}
2327
EOF
2428
nix run --no-write-lock-file .#appAsApp
2529
nix run --no-write-lock-file .#pkgAsPkg
30+
nix run --no-write-lock-file .#functionApp
2631

2732
! nix run --no-write-lock-file .#pkgAsApp || fail "'nix run' shouldn’t accept an 'app' defined under 'packages'"
2833
! nix run --no-write-lock-file .#appAsPkg || fail "elements of 'apps' should be of type 'app'"
@@ -87,4 +92,3 @@ nix run --no-write-lock-file -- . myarg1 myarg2 2>&1 | grepQuiet "ARGS: myarg1 m
8792

8893
# And verify that a non-installable first argument causes an error
8994
expectStderr 1 nix run --no-write-lock-file -- myarg1 myarg2 | grepQuiet "error.*myarg1"
90-

tests/functional/search.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,16 @@ e=$'\x1b' # grep doesn't support \e, \033 or even \x1b
4646
(( $(nix search -f search.nix foo ^ --exclude 'foo|bar' | grep -Ec 'foo|bar') == 0 ))
4747
(( $(nix search -f search.nix foo ^ -e foo --exclude bar | grep -Ec 'foo|bar') == 0 ))
4848
[[ $(nix search -f search.nix '' ^ -e bar --json | jq -c 'keys') == '["foo","hello"]' ]]
49+
50+
# Flake installables with function-valued package sets are auto-called.
51+
flakeDir="$TEST_HOME/function-flake"
52+
mkdir "$flakeDir"
53+
cp search.nix "$config_nix" "$flakeDir"
54+
cat > "$flakeDir/flake.nix" <<EOF
55+
{
56+
outputs = { self }: {
57+
packages.$system = {}: import ./search.nix;
58+
};
59+
}
60+
EOF
61+
nix search "$flakeDir" hello | grepQuiet hello

0 commit comments

Comments
 (0)