From 1deca0cd048bbbd1a3c6fd03ce53d8cbd98a5bf2 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 25 Nov 2024 15:11:40 +0100 Subject: [PATCH] builtins.getFlake: Allow inputs to overridden 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. --- src/libflake/flake/flake.cc | 91 ++++++++++++++++++++++------ src/libflake/flake/flake.hh | 8 ++- tests/functional/flakes/get-flake.sh | 20 ++++++ 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake/flake.cc index 19b622a34af..688b7de5ba1 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake/flake.cc @@ -99,7 +99,7 @@ static std::map parseFlakeInputs( const std::optional & baseDir, InputPath lockRootPath); static FlakeInput parseFlakeInput(EvalState & state, - std::string_view inputName, Value * value, const PosIdx pos, + std::optional inputName, Value * value, const PosIdx pos, const std::optional & baseDir, InputPath lockRootPath) { expectType(state, nAttrs, *value, pos); @@ -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; } @@ -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; } } @@ -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 recurse; + + recurse = [&](const InputPath & inputPath, const FlakeInput & input) + { + if (!input.ref) + state.error("'builtins.getFlake' requires attribute 'url'") + .atPos(*args[0]) + .debugThrow(); + if (input.follows) + state.error("'builtins.getFlake' does not permit attribute 'follows'") + .atPos(*args[0]) + .debugThrow(); + if (!input.isFlake) + state.error("'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({ @@ -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, diff --git a/src/libflake/flake/flake.hh b/src/libflake/flake/flake.hh index cc2bea76e59..8699fe74cfb 100644 --- a/src/libflake/flake/flake.hh +++ b/src/libflake/flake/flake.hh @@ -92,7 +92,7 @@ struct Flake */ SourcePath path; /** - * pretend that 'lockedRef' is dirty + * Pretend that 'lockedRef' is dirty. */ bool forceDirty = false; std::optional description; @@ -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'. diff --git a/tests/functional/flakes/get-flake.sh b/tests/functional/flakes/get-flake.sh index b69c2812a07..58c446f7e88 100644 --- a/tests/functional/flakes/get-flake.sh +++ b/tests/functional/flakes/get-flake.sh @@ -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"