Skip to content

Commit

Permalink
Add inputs.self.submodules flake attribute
Browse files Browse the repository at this point in the history
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 NixOS#5312, NixOS#9842.
  • Loading branch information
edolstra committed Feb 4, 2025
1 parent 0159848 commit 25fcc8d
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 48 deletions.
147 changes: 100 additions & 47 deletions src/libflake/flake/flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,47 @@ static void expectType(EvalState & state, ValueType type,
showType(type), showType(value.type()), state.positions[pos]);
}

static std::map<FlakeId, FlakeInput> parseFlakeInputs(
static std::pair<std::map<FlakeId, FlakeInput>, fetchers::Attrs> parseFlakeInputs(
EvalState & state,
Value * value,
const PosIdx pos,
const InputAttrPath & lockRootAttrPath,
const SourcePath & flakeDir);
const SourcePath & flakeDir,
bool allowSelf);

static void parseFlakeInputAttr(
EvalState & state,
const Attr & attr,
fetchers::Attrs & attrs)
{
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (attr.value->type()) {
case nString:
attrs.emplace(state.symbols[attr.name], attr.value->c_str());
break;
case nBool:
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() });
break;
case nInt: {
auto intValue = attr.value->integer().value;
if (intValue < 0)
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
break;
}
default:
if (attr.name == state.symbols.create("publicKeys")) {
experimentalFeatureSettings.require(Xp::VerifiedFetches);
NixStringContext emptyContext = {};
attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, attr.pos, emptyContext).dump());
} else
state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
state.symbols[attr.name], showType(*attr.value)).debugThrow();
}
#pragma GCC diagnostic pop
}

static FlakeInput parseFlakeInput(
EvalState & state,
Expand Down Expand Up @@ -166,44 +201,14 @@ static FlakeInput parseFlakeInput(
expectType(state, nBool, *attr.value, attr.pos);
input.isFlake = attr.value->boolean();
} else if (attr.name == sInputs) {
input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir);
input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir, false).first;
} else if (attr.name == sFollows) {
expectType(state, nString, *attr.value, attr.pos);
auto follows(parseInputAttrPath(attr.value->c_str()));
follows.insert(follows.begin(), lockRootAttrPath.begin(), lockRootAttrPath.end());
input.follows = follows;
} else {
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (attr.value->type()) {
case nString:
attrs.emplace(state.symbols[attr.name], attr.value->c_str());
break;
case nBool:
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() });
break;
case nInt: {
auto intValue = attr.value->integer().value;

if (intValue < 0) {
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
}

attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
break;
}
default:
if (attr.name == state.symbols.create("publicKeys")) {
experimentalFeatureSettings.require(Xp::VerifiedFetches);
NixStringContext emptyContext = {};
attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, emptyContext).dump());
} else
state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
state.symbols[attr.name], showType(*attr.value)).debugThrow();
}
#pragma GCC diagnostic pop
}
} else
parseFlakeInputAttr(state, attr, attrs);
} catch (Error & e) {
e.addTrace(
state.positions[attr.pos],
Expand Down Expand Up @@ -233,28 +238,39 @@ static FlakeInput parseFlakeInput(
return input;
}

static std::map<FlakeId, FlakeInput> parseFlakeInputs(
static std::pair<std::map<FlakeId, FlakeInput>, fetchers::Attrs> parseFlakeInputs(
EvalState & state,
Value * value,
const PosIdx pos,
const InputAttrPath & lockRootAttrPath,
const SourcePath & flakeDir)
const SourcePath & flakeDir,
bool allowSelf)
{
std::map<FlakeId, FlakeInput> inputs;
fetchers::Attrs selfAttrs;

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

for (auto & inputAttr : *value->attrs()) {
inputs.emplace(state.symbols[inputAttr.name],
parseFlakeInput(state,
state.symbols[inputAttr.name],
inputAttr.value,
inputAttr.pos,
lockRootAttrPath,
flakeDir));
auto inputName = state.symbols[inputAttr.name];
if (inputName == "self") {
if (!allowSelf)
throw Error("'self' input attribute not allowed at %s", state.positions[inputAttr.pos]);
expectType(state, nAttrs, *inputAttr.value, inputAttr.pos);
for (auto & attr : *inputAttr.value->attrs())
parseFlakeInputAttr(state, attr, selfAttrs);
} else {
inputs.emplace(inputName,
parseFlakeInput(state,
inputName,
inputAttr.value,
inputAttr.pos,
lockRootAttrPath,
flakeDir));
}
}

return inputs;
return {inputs, selfAttrs};
}

static Flake readFlake(
Expand Down Expand Up @@ -286,8 +302,11 @@ static Flake readFlake(

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

if (auto inputs = vInfo.attrs()->get(sInputs))
flake.inputs = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootAttrPath, flakeDir);
if (auto inputs = vInfo.attrs()->get(sInputs)) {
auto [flakeInputs, selfAttrs] = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootAttrPath, flakeDir, true);
flake.inputs = std::move(flakeInputs);
flake.selfAttrs = std::move(selfAttrs);
}

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

Expand Down Expand Up @@ -361,6 +380,23 @@ static Flake readFlake(
return flake;
}

static FlakeRef applySelfAttrs(
const FlakeRef & ref,
const Flake & flake)
{
auto newRef(ref);

std::set<std::string> allowedAttrs{"submodules"};

for (auto & attr : flake.selfAttrs) {
if (!allowedAttrs.contains(attr.first))
throw Error("flake 'self' attribute '%s' is not supported", attr.first);
newRef.input.attrs.insert_or_assign(attr.first, attr.second);
}

return newRef;
}

static Flake getFlake(
EvalState & state,
const FlakeRef & originalRef,
Expand All @@ -372,6 +408,22 @@ static Flake getFlake(
auto [accessor, resolvedRef, lockedRef] = fetchOrSubstituteTree(
state, originalRef, useRegistries, flakeCache);

// Parse/eval flake.nix to get at the input.self attributes.
auto flake = readFlake(state, originalRef, resolvedRef, lockedRef, {accessor}, lockRootAttrPath);

// Re-fetch the tree if necessary.
auto newLockedRef = applySelfAttrs(lockedRef, flake);

if (lockedRef != newLockedRef) {
debug("refetching input '%s' due to self attribute", newLockedRef);
// FIXME: need to remove attrs that are invalidated by the changed input attrs, such as 'narHash'.
newLockedRef.input.attrs.erase("narHash");
auto [accessor2, resolvedRef2, lockedRef2] = fetchOrSubstituteTree(
state, newLockedRef, false, flakeCache);
accessor = accessor2;
lockedRef = lockedRef2;
}

// Copy the tree to the store.
auto storePath = copyInputToStore(state, lockedRef.input, accessor);

Expand Down Expand Up @@ -492,6 +544,7 @@ LockedFlake lockFlake(
/* Get the overrides (i.e. attributes of the form
'inputs.nixops.inputs.nixpkgs.url = ...'). */
for (auto & [id, input] : flakeInputs) {
//if (id == "self") continue;
for (auto & [idOverride, inputOverride] : input.overrides) {
auto inputAttrPath(inputAttrPathPrefix);
inputAttrPath.push_back(id);
Expand Down
15 changes: 14 additions & 1 deletion src/libflake/flake/flake.hh
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,37 @@ struct Flake
* The original flake specification (by the user)
*/
FlakeRef originalRef;

/**
* registry references and caching resolved to the specific underlying flake
*/
FlakeRef resolvedRef;

/**
* the specific local store result of invoking the fetcher
*/
FlakeRef lockedRef;

/**
* The path of `flake.nix`.
*/
SourcePath path;

/**
* pretend that 'lockedRef' is dirty
* Pretend that `lockedRef` is dirty.
*/
bool forceDirty = false;

std::optional<std::string> description;

FlakeInputs inputs;

/**
* Attributes to be retroactively applied to the `self` input
* (such as `submodules = true`).
*/
fetchers::Attrs selfAttrs;

/**
* 'nixConfig' attribute
*/
Expand Down
14 changes: 14 additions & 0 deletions tests/functional/flakes/flake-in-submodule.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ git -C "$rootRepo" commit -m "Add flake.nix"
storePath=$(nix flake prefetch --json "$rootRepo?submodules=1" | jq -r .storePath)
[[ -e "$storePath/submodule" ]]

# Test the use of inputs.self.
cat > "$rootRepo"/flake.nix <<EOF
{
inputs.self.submodules = true;
outputs = { self }: {
foo = self.outPath;
};
}
EOF
git -C "$rootRepo" commit -a -m "Bla"

storePath=$(nix eval --raw "$rootRepo#foo")
[[ -e "$storePath/submodule" ]]

# The root repo may use the submodule repo as an input
# through the relative path. This may change in the future;
# see: https://discourse.nixos.org/t/57783 and #9708.
Expand Down

0 comments on commit 25fcc8d

Please sign in to comment.