Skip to content

Commit

Permalink
builtins.getFlake: Allow inputs to overridden
Browse files Browse the repository at this point in the history
This uses the same syntax as flake inputs in flake.nix, e.g.

  builtins.getFlake {
    url = "github:NixOS/nix/55bc52401966fbffa525c574c14f67b00bc4fb3a";
    inputs.nixpkgs.url = "github:NixOS/nixpkgs/c69a9bffbecde46b4b939465422ddc59493d3e4d";
  }

Note that currently it's not supported to set `flake = false` or to
use `follows` (because lockFlake() doesn't support that for CLI
overrides), but it could be implemented in the future.

Fixes #9154.
  • Loading branch information
edolstra committed Nov 25, 2024
1 parent 2b62625 commit 1deca0c
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 19 deletions.
91 changes: 73 additions & 18 deletions src/libflake/flake/flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ static std::map<FlakeId, FlakeInput> parseFlakeInputs(
const std::optional<Path> & baseDir, InputPath lockRootPath);

static FlakeInput parseFlakeInput(EvalState & state,
std::string_view inputName, Value * value, const PosIdx pos,
std::optional<std::string_view> inputName, Value * value, const PosIdx pos,
const std::optional<Path> & baseDir, InputPath lockRootPath)
{
expectType(state, nAttrs, *value, pos);
Expand Down Expand Up @@ -185,8 +185,8 @@ static FlakeInput parseFlakeInput(EvalState & state,
input.ref = parseFlakeRef(state.fetchSettings, *url, baseDir, true, input.isFlake);
}

if (!input.follows && !input.ref)
input.ref = FlakeRef::fromAttrs(state.fetchSettings, {{"type", "indirect"}, {"id", std::string(inputName)}});
if (inputName && !input.follows && !input.ref)
input.ref = FlakeRef::fromAttrs(state.fetchSettings, {{"type", "indirect"}, {"id", std::string(*inputName)}});

return input;
}
Expand Down Expand Up @@ -735,7 +735,10 @@ LockedFlake lockFlake(
} else
throw Error("cannot write modified lock file of flake '%s' (use '--no-write-lock-file' to ignore)", topRef);
} else {
warn("not writing modified lock file of flake '%s':\n%s", topRef, chomp(diff));
if (lockFlags.warnModifiedLockFile)
warn("not writing modified lock file of flake '%s':\n%s", topRef, chomp(diff));
else
debug("not writing modified lock file of flake '%s':\n%s", topRef, chomp(diff));
flake.forceDirty = true;
}
}
Expand Down Expand Up @@ -823,20 +826,63 @@ void initLib(const Settings & settings)
{
auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));
auto flakeRef = parseFlakeRef(state.fetchSettings, flakeRefS, {}, true);
if (state.settings.pureEval && !flakeRef.input.isLocked())
throw Error("cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", flakeRefS, state.positions[pos]);

callFlake(state,
lockFlake(settings, state, flakeRef,
LockFlags {
.updateLockFile = false,
.writeLockFile = false,
.useRegistries = !state.settings.pureEval && settings.useRegistries,
.allowUnlocked = !state.settings.pureEval,
}),
v);
state.forceValue(*args[0], pos);

LockFlags lockFlags {
.updateLockFile = false,
.writeLockFile = false,
.warnModifiedLockFile = false,
.useRegistries = !state.settings.pureEval && settings.useRegistries,
.allowUnlocked = !state.settings.pureEval,
};

auto flakeRef =
args[0]->type() == nString
? ({
std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));
auto flakeRef = parseFlakeRef(state.fetchSettings, flakeRefS, {}, true);
if (state.settings.pureEval && !flakeRef.input.isLocked())
throw Error("cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", flakeRefS, state.positions[pos]);
flakeRef;
})
: ({
auto flakeInput = parseFlakeInput(state, std::nullopt, args[0], pos, {}, {});

/* Convert the result of parseFlakeInput() into a
overrides map and a top-level flakeref. */
std::function<void(const InputPath & inputPath, const FlakeInput & input)> recurse;

recurse = [&](const InputPath & inputPath, const FlakeInput & input)
{
if (!input.ref)
state.error<EvalError>("'builtins.getFlake' requires attribute 'url'")
.atPos(*args[0])
.debugThrow();
if (input.follows)
state.error<EvalError>("'builtins.getFlake' does not permit attribute 'follows'")
.atPos(*args[0])
.debugThrow();
if (!input.isFlake)
state.error<EvalError>("'builtins.getFlake' does not permit attribute 'flake = false'; use 'builtins.fetchTree' instead")
.atPos(*args[0])
.debugThrow();

for (auto & [inputName, input2] : input.overrides) {
auto inputPath2{inputPath};
inputPath2.push_back(inputName);

recurse(inputPath2, input2);

lockFlags.inputOverrides.insert_or_assign(inputPath2, input2.ref.value());
}
};

recurse({}, flakeInput);

flakeInput.ref.value();
});

callFlake(state, lockFlake(settings, state, flakeRef, lockFlags), v);
};

RegisterPrimOp::primOps->push_back({
Expand All @@ -856,6 +902,15 @@ void initLib(const Settings & settings)
```nix
(builtins.getFlake "github:edolstra/dwarffs").rev
```
It is possible to override inputs of the flake using the same syntax to specify flake inputs in `flake.nix`, e.g.
```nix
builtins.getFlake {
url = "github:NixOS/nix/55bc52401966fbffa525c574c14f67b00bc4fb3a";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/c69a9bffbecde46b4b939465422ddc59493d3e4d";
}
```
)",
.fun = prim_getFlake,
.experimentalFeature = Xp::Flakes,
Expand Down
8 changes: 7 additions & 1 deletion src/libflake/flake/flake.hh
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ struct Flake
*/
SourcePath path;
/**
* pretend that 'lockedRef' is dirty
* Pretend that 'lockedRef' is dirty.
*/
bool forceDirty = false;
std::optional<std::string> description;
Expand Down Expand Up @@ -156,6 +156,12 @@ struct LockFlags
*/
bool writeLockFile = true;

/**
* When `writeLockFile` is false, whether we're warning about
* modified lock files.
*/
bool warnModifiedLockFile = true;

/**
* Whether to use the registries to lookup indirect flake
* references like 'nixpkgs'.
Expand Down
20 changes: 20 additions & 0 deletions tests/functional/flakes/get-flake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"git+file://$flake1

# But should succeed in impure mode.
nix eval --expr "builtins.getFlake \"$flake2Dir\"" --impure

# Test overrides in getFlake.
flake1Copy="$flake1Dir-copy"
rm -rf "$flake1Copy"
cp -r "$flake1Dir" "$flake1Copy"
sed -i "$flake1Copy/simple.builder.sh" -e 's/World/Universe/'

# Should fail in pure mode since the override is unlocked.
(! nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake { url = \"$flake2Dir\"; inputs.flake1.url = \"$flake1Copy\"; }).packages.$system.bar")

# Should succeed in impure mode.
nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake { url = \"$flake2Dir\"; inputs.flake1.url = \"$flake1Copy\"; }).packages.$system.bar" --impure
[[ $(cat "$TEST_ROOT/result/hello") = 'Hello Universe!' ]]

# Should succeed if we lock the override.
git -C "$flake1Copy" commit -a -m 'bla'

flake1CopyLocked="$(nix flake metadata --json "$flake1Copy" | jq -r .url)"

nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake { url = \"$flake2Dir\"; inputs.flake1.url = \"$flake1CopyLocked\"; }).packages.$system.bar"

0 comments on commit 1deca0c

Please sign in to comment.