Skip to content

Commit 25fcc8d

Browse files
committed
Add inputs.self.submodules flake attribute
This allows a flake to specify that it needs Git submodules to be enabled (or disabled, if we ever change the default) on the top-level flake. This requires the input to be refetched, but since the first fetch is lazy, this shouldn't be expensive. Currently the only attribute allowed by `inputs.self` is `submodules`, but more can be added in the future (e.g. a `lazy` attribute to opt in to lazy tree behaviour). Fixes #5312, #9842.
1 parent 0159848 commit 25fcc8d

File tree

3 files changed

+128
-48
lines changed

3 files changed

+128
-48
lines changed

src/libflake/flake/flake.cc

Lines changed: 100 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,47 @@ static void expectType(EvalState & state, ValueType type,
116116
showType(type), showType(value.type()), state.positions[pos]);
117117
}
118118

119-
static std::map<FlakeId, FlakeInput> parseFlakeInputs(
119+
static std::pair<std::map<FlakeId, FlakeInput>, fetchers::Attrs> parseFlakeInputs(
120120
EvalState & state,
121121
Value * value,
122122
const PosIdx pos,
123123
const InputAttrPath & lockRootAttrPath,
124-
const SourcePath & flakeDir);
124+
const SourcePath & flakeDir,
125+
bool allowSelf);
126+
127+
static void parseFlakeInputAttr(
128+
EvalState & state,
129+
const Attr & attr,
130+
fetchers::Attrs & attrs)
131+
{
132+
// Allow selecting a subset of enum values
133+
#pragma GCC diagnostic push
134+
#pragma GCC diagnostic ignored "-Wswitch-enum"
135+
switch (attr.value->type()) {
136+
case nString:
137+
attrs.emplace(state.symbols[attr.name], attr.value->c_str());
138+
break;
139+
case nBool:
140+
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() });
141+
break;
142+
case nInt: {
143+
auto intValue = attr.value->integer().value;
144+
if (intValue < 0)
145+
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
146+
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
147+
break;
148+
}
149+
default:
150+
if (attr.name == state.symbols.create("publicKeys")) {
151+
experimentalFeatureSettings.require(Xp::VerifiedFetches);
152+
NixStringContext emptyContext = {};
153+
attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, attr.pos, emptyContext).dump());
154+
} else
155+
state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
156+
state.symbols[attr.name], showType(*attr.value)).debugThrow();
157+
}
158+
#pragma GCC diagnostic pop
159+
}
125160

126161
static FlakeInput parseFlakeInput(
127162
EvalState & state,
@@ -166,44 +201,14 @@ static FlakeInput parseFlakeInput(
166201
expectType(state, nBool, *attr.value, attr.pos);
167202
input.isFlake = attr.value->boolean();
168203
} else if (attr.name == sInputs) {
169-
input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir);
204+
input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir, false).first;
170205
} else if (attr.name == sFollows) {
171206
expectType(state, nString, *attr.value, attr.pos);
172207
auto follows(parseInputAttrPath(attr.value->c_str()));
173208
follows.insert(follows.begin(), lockRootAttrPath.begin(), lockRootAttrPath.end());
174209
input.follows = follows;
175-
} else {
176-
// Allow selecting a subset of enum values
177-
#pragma GCC diagnostic push
178-
#pragma GCC diagnostic ignored "-Wswitch-enum"
179-
switch (attr.value->type()) {
180-
case nString:
181-
attrs.emplace(state.symbols[attr.name], attr.value->c_str());
182-
break;
183-
case nBool:
184-
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() });
185-
break;
186-
case nInt: {
187-
auto intValue = attr.value->integer().value;
188-
189-
if (intValue < 0) {
190-
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
191-
}
192-
193-
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
194-
break;
195-
}
196-
default:
197-
if (attr.name == state.symbols.create("publicKeys")) {
198-
experimentalFeatureSettings.require(Xp::VerifiedFetches);
199-
NixStringContext emptyContext = {};
200-
attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, emptyContext).dump());
201-
} else
202-
state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
203-
state.symbols[attr.name], showType(*attr.value)).debugThrow();
204-
}
205-
#pragma GCC diagnostic pop
206-
}
210+
} else
211+
parseFlakeInputAttr(state, attr, attrs);
207212
} catch (Error & e) {
208213
e.addTrace(
209214
state.positions[attr.pos],
@@ -233,28 +238,39 @@ static FlakeInput parseFlakeInput(
233238
return input;
234239
}
235240

236-
static std::map<FlakeId, FlakeInput> parseFlakeInputs(
241+
static std::pair<std::map<FlakeId, FlakeInput>, fetchers::Attrs> parseFlakeInputs(
237242
EvalState & state,
238243
Value * value,
239244
const PosIdx pos,
240245
const InputAttrPath & lockRootAttrPath,
241-
const SourcePath & flakeDir)
246+
const SourcePath & flakeDir,
247+
bool allowSelf)
242248
{
243249
std::map<FlakeId, FlakeInput> inputs;
250+
fetchers::Attrs selfAttrs;
244251

245252
expectType(state, nAttrs, *value, pos);
246253

247254
for (auto & inputAttr : *value->attrs()) {
248-
inputs.emplace(state.symbols[inputAttr.name],
249-
parseFlakeInput(state,
250-
state.symbols[inputAttr.name],
251-
inputAttr.value,
252-
inputAttr.pos,
253-
lockRootAttrPath,
254-
flakeDir));
255+
auto inputName = state.symbols[inputAttr.name];
256+
if (inputName == "self") {
257+
if (!allowSelf)
258+
throw Error("'self' input attribute not allowed at %s", state.positions[inputAttr.pos]);
259+
expectType(state, nAttrs, *inputAttr.value, inputAttr.pos);
260+
for (auto & attr : *inputAttr.value->attrs())
261+
parseFlakeInputAttr(state, attr, selfAttrs);
262+
} else {
263+
inputs.emplace(inputName,
264+
parseFlakeInput(state,
265+
inputName,
266+
inputAttr.value,
267+
inputAttr.pos,
268+
lockRootAttrPath,
269+
flakeDir));
270+
}
255271
}
256272

257-
return inputs;
273+
return {inputs, selfAttrs};
258274
}
259275

260276
static Flake readFlake(
@@ -286,8 +302,11 @@ static Flake readFlake(
286302

287303
auto sInputs = state.symbols.create("inputs");
288304

289-
if (auto inputs = vInfo.attrs()->get(sInputs))
290-
flake.inputs = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootAttrPath, flakeDir);
305+
if (auto inputs = vInfo.attrs()->get(sInputs)) {
306+
auto [flakeInputs, selfAttrs] = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootAttrPath, flakeDir, true);
307+
flake.inputs = std::move(flakeInputs);
308+
flake.selfAttrs = std::move(selfAttrs);
309+
}
291310

292311
auto sOutputs = state.symbols.create("outputs");
293312

@@ -361,6 +380,23 @@ static Flake readFlake(
361380
return flake;
362381
}
363382

383+
static FlakeRef applySelfAttrs(
384+
const FlakeRef & ref,
385+
const Flake & flake)
386+
{
387+
auto newRef(ref);
388+
389+
std::set<std::string> allowedAttrs{"submodules"};
390+
391+
for (auto & attr : flake.selfAttrs) {
392+
if (!allowedAttrs.contains(attr.first))
393+
throw Error("flake 'self' attribute '%s' is not supported", attr.first);
394+
newRef.input.attrs.insert_or_assign(attr.first, attr.second);
395+
}
396+
397+
return newRef;
398+
}
399+
364400
static Flake getFlake(
365401
EvalState & state,
366402
const FlakeRef & originalRef,
@@ -372,6 +408,22 @@ static Flake getFlake(
372408
auto [accessor, resolvedRef, lockedRef] = fetchOrSubstituteTree(
373409
state, originalRef, useRegistries, flakeCache);
374410

411+
// Parse/eval flake.nix to get at the input.self attributes.
412+
auto flake = readFlake(state, originalRef, resolvedRef, lockedRef, {accessor}, lockRootAttrPath);
413+
414+
// Re-fetch the tree if necessary.
415+
auto newLockedRef = applySelfAttrs(lockedRef, flake);
416+
417+
if (lockedRef != newLockedRef) {
418+
debug("refetching input '%s' due to self attribute", newLockedRef);
419+
// FIXME: need to remove attrs that are invalidated by the changed input attrs, such as 'narHash'.
420+
newLockedRef.input.attrs.erase("narHash");
421+
auto [accessor2, resolvedRef2, lockedRef2] = fetchOrSubstituteTree(
422+
state, newLockedRef, false, flakeCache);
423+
accessor = accessor2;
424+
lockedRef = lockedRef2;
425+
}
426+
375427
// Copy the tree to the store.
376428
auto storePath = copyInputToStore(state, lockedRef.input, accessor);
377429

@@ -492,6 +544,7 @@ LockedFlake lockFlake(
492544
/* Get the overrides (i.e. attributes of the form
493545
'inputs.nixops.inputs.nixpkgs.url = ...'). */
494546
for (auto & [id, input] : flakeInputs) {
547+
//if (id == "self") continue;
495548
for (auto & [idOverride, inputOverride] : input.overrides) {
496549
auto inputAttrPath(inputAttrPathPrefix);
497550
inputAttrPath.push_back(id);

src/libflake/flake/flake.hh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,37 @@ struct Flake
7979
* The original flake specification (by the user)
8080
*/
8181
FlakeRef originalRef;
82+
8283
/**
8384
* registry references and caching resolved to the specific underlying flake
8485
*/
8586
FlakeRef resolvedRef;
87+
8688
/**
8789
* the specific local store result of invoking the fetcher
8890
*/
8991
FlakeRef lockedRef;
92+
9093
/**
9194
* The path of `flake.nix`.
9295
*/
9396
SourcePath path;
97+
9498
/**
95-
* pretend that 'lockedRef' is dirty
99+
* Pretend that `lockedRef` is dirty.
96100
*/
97101
bool forceDirty = false;
102+
98103
std::optional<std::string> description;
104+
99105
FlakeInputs inputs;
106+
107+
/**
108+
* Attributes to be retroactively applied to the `self` input
109+
* (such as `submodules = true`).
110+
*/
111+
fetchers::Attrs selfAttrs;
112+
100113
/**
101114
* 'nixConfig' attribute
102115
*/

tests/functional/flakes/flake-in-submodule.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ git -C "$rootRepo" commit -m "Add flake.nix"
7777
storePath=$(nix flake prefetch --json "$rootRepo?submodules=1" | jq -r .storePath)
7878
[[ -e "$storePath/submodule" ]]
7979

80+
# Test the use of inputs.self.
81+
cat > "$rootRepo"/flake.nix <<EOF
82+
{
83+
inputs.self.submodules = true;
84+
outputs = { self }: {
85+
foo = self.outPath;
86+
};
87+
}
88+
EOF
89+
git -C "$rootRepo" commit -a -m "Bla"
90+
91+
storePath=$(nix eval --raw "$rootRepo#foo")
92+
[[ -e "$storePath/submodule" ]]
93+
8094
# The root repo may use the submodule repo as an input
8195
# through the relative path. This may change in the future;
8296
# see: https://discourse.nixos.org/t/57783 and #9708.

0 commit comments

Comments
 (0)