From 2b6262538b46acb2c3af9fa2e660add97a7641b1 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 25 Nov 2024 13:50:34 +0100 Subject: [PATCH 1/4] Move getFlake tests into a separate file --- tests/functional/flakes/flakes.sh | 10 ---------- tests/functional/flakes/get-flake.sh | 23 +++++++++++++++++++++++ tests/functional/flakes/meson.build | 1 + 3 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 tests/functional/flakes/get-flake.sh diff --git a/tests/functional/flakes/flakes.sh b/tests/functional/flakes/flakes.sh index de37ae1b781..5332b1bcce4 100755 --- a/tests/functional/flakes/flakes.sh +++ b/tests/functional/flakes/flakes.sh @@ -109,14 +109,6 @@ nix build -o "$TEST_ROOT/result" "git+file://$flake1Dir?ref=HEAD#default" nix build -o "$flake1Dir/result" "git+file://$flake1Dir" nix path-info "$flake1Dir/result" -# 'getFlake' on an unlocked flakeref should fail in pure mode, but -# succeed in impure mode. -(! nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default") -nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default" --impure - -# 'getFlake' on a locked flakeref should succeed even in pure mode. -nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"git+file://$flake1Dir?rev=$hash2\").packages.$system.default" - # Regression test for dirOf on the root of the flake. [[ $(nix eval --json flake1#parent) = \""$NIX_STORE_DIR"\" ]] @@ -131,12 +123,10 @@ nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"git+file://$flake1 # Building a flake with an unlocked dependency should fail in pure mode. (! nix build -o "$TEST_ROOT/result" flake2#bar --no-registries) (! nix build -o "$TEST_ROOT/result" flake2#bar --no-use-registries) -(! nix eval --expr "builtins.getFlake \"$flake2Dir\"") # But should succeed in impure mode. (! nix build -o "$TEST_ROOT/result" flake2#bar --impure) nix build -o "$TEST_ROOT/result" flake2#bar --impure --no-write-lock-file -nix eval --expr "builtins.getFlake \"$flake2Dir\"" --impure # Building a local flake with an unlocked dependency should fail with --no-update-lock-file. expect 1 nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --no-update-lock-file 2>&1 | grep 'requires lock file changes' diff --git a/tests/functional/flakes/get-flake.sh b/tests/functional/flakes/get-flake.sh new file mode 100644 index 00000000000..b69c2812a07 --- /dev/null +++ b/tests/functional/flakes/get-flake.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source ./common.sh + +TODO_NixOS + +createFlake1 +createFlake2 + +# 'getFlake' on an unlocked flakeref should fail in pure mode, but +# succeed in impure mode. +(! nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default") +nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default" --impure + +# 'getFlake' on a locked flakeref should succeed even in pure mode. +hash=$(nix flake metadata flake1 --json --refresh | jq -r .revision) +nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"git+file://$flake1Dir?rev=$hash\").packages.$system.default" + +# Building a flake with an unlocked dependency should fail in pure mode. +(! nix eval --expr "builtins.getFlake \"$flake2Dir\"") + +# But should succeed in impure mode. +nix eval --expr "builtins.getFlake \"$flake2Dir\"" --impure diff --git a/tests/functional/flakes/meson.build b/tests/functional/flakes/meson.build index 00060e3c927..603e6236f29 100644 --- a/tests/functional/flakes/meson.build +++ b/tests/functional/flakes/meson.build @@ -27,6 +27,7 @@ suites += { 'shebang.sh', 'commit-lock-file-summary.sh', 'non-flake-inputs.sh', + 'get-flake.sh', ], 'workdir': meson.current_source_dir(), } From 1deca0cd048bbbd1a3c6fd03ce53d8cbd98a5bf2 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 25 Nov 2024 15:11:40 +0100 Subject: [PATCH 2/4] 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" From 78abd1165fe91bf1aa18a4ff106dd5b788bad028 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 26 Nov 2024 13:09:49 +0100 Subject: [PATCH 3/4] Don't mention fetchTree --- src/libflake/flake/flake.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake/flake.cc index 688b7de5ba1..000829bb19b 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake/flake.cc @@ -863,7 +863,7 @@ void initLib(const Settings & settings) .atPos(*args[0]) .debugThrow(); if (!input.isFlake) - state.error("'builtins.getFlake' does not permit attribute 'flake = false'; use 'builtins.fetchTree' instead") + state.error("'builtins.getFlake' does not permit attribute 'flake = false'") .atPos(*args[0]) .debugThrow(); From 8fd798a2282d926981012bfc7d0d8d9527ea8612 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 9 Dec 2024 16:32:04 +0100 Subject: [PATCH 4/4] Add release note --- doc/manual/rl-next/get-flake-overrides.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/manual/rl-next/get-flake-overrides.md diff --git a/doc/manual/rl-next/get-flake-overrides.md b/doc/manual/rl-next/get-flake-overrides.md new file mode 100644 index 00000000000..19b3a9515d1 --- /dev/null +++ b/doc/manual/rl-next/get-flake-overrides.md @@ -0,0 +1,13 @@ +--- +synopsis: "`builtins.getFlake` allows inputs to be overriden" +prs: [11952] +--- + +`builtins.getFlake` now allows you to override the inputs of a flake, using the same override mechanism used for flake inputs in `flake.nix`. For example, to fetch and call the `NixOS/nix` flake and override its `nixpkgs` input: + +```nix +builtins.getFlake { + url = "github:NixOS/nix/55bc52401966fbffa525c574c14f67b00bc4fb3a"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/c69a9bffbecde46b4b939465422ddc59493d3e4d"; +} +```