diff --git a/corepkgs/call-flake.nix b/corepkgs/call-flake.nix new file mode 100644 index 000000000..29ff41040 --- /dev/null +++ b/corepkgs/call-flake.nix @@ -0,0 +1,22 @@ +locks: rootSrc: + +let + + callFlake = sourceInfo: locks: + let + flake = import (sourceInfo + "/flake.nix"); + + inputs = builtins.mapAttrs (n: v: + if v.flake or true + then callFlake (fetchTree v.locked) v.inputs + else fetchTree v.locked) locks; + + outputs = flake.outputs (inputs // { self = result; }); + + result = outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; }; + in + assert flake.edition == 201909; + + result; + +in callFlake rootSrc (builtins.fromJSON locks).inputs diff --git a/corepkgs/local.mk b/corepkgs/local.mk index 67306e50d..fb44e7c3e 100644 --- a/corepkgs/local.mk +++ b/corepkgs/local.mk @@ -3,7 +3,8 @@ corepkgs_FILES = \ unpack-channel.nix \ derivation.nix \ fetchurl.nix \ - imported-drv-to-derivation.nix + imported-drv-to-derivation.nix \ + call-flake.nix $(foreach file,config.nix $(corepkgs_FILES),$(eval $(call install-data-in,$(d)/$(file),$(datadir)/nix/corepkgs))) diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 4fa125f1b..eac7d026d 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -6,10 +6,6 @@ #include "fetchers/fetchers.hh" #include "finally.hh" -#include -#include -#include - namespace nix { using namespace flake; @@ -158,7 +154,8 @@ static FlakeInput parseFlakeInput(EvalState & state, if (attr.value->type == tString) attrs.emplace(attr.name, attr.value->string.s); else - throw Error("unsupported attribute type"); + throw TypeError("flake input attribute '%s' is %s while a string is expected", + attr.name, showType(*attr.value)); } } catch (Error & e) { e.addPrefix(fmt("in flake attribute '%s' at '%s':\n", attr.name, *attr.pos)); @@ -621,143 +618,23 @@ LockedFlake lockFlake( return LockedFlake { .flake = std::move(flake), .lockFile = std::move(newLockFile) }; } -static void emitSourceInfoAttrs( - EvalState & state, - const FlakeRef & flakeRef, - const fetchers::Tree & sourceInfo, - Value & vAttrs) -{ - assert(state.store->isValidPath(sourceInfo.storePath)); - auto pathS = state.store->printStorePath(sourceInfo.storePath); - mkString(*state.allocAttr(vAttrs, state.sOutPath), pathS, {pathS}); - - assert(sourceInfo.info.narHash); - mkString(*state.allocAttr(vAttrs, state.symbols.create("narHash")), - sourceInfo.info.narHash.to_string(SRI)); - - if (auto rev = flakeRef.input->getRev()) { - mkString(*state.allocAttr(vAttrs, state.symbols.create("rev")), - rev->gitRev()); - mkString(*state.allocAttr(vAttrs, state.symbols.create("shortRev")), - rev->gitShortRev()); - } - - if (sourceInfo.info.revCount) - mkInt(*state.allocAttr(vAttrs, state.symbols.create("revCount")), *sourceInfo.info.revCount); - - if (sourceInfo.info.lastModified) - mkString(*state.allocAttr(vAttrs, state.symbols.create("lastModified")), - fmt("%s", std::put_time(std::gmtime(&*sourceInfo.info.lastModified), "%Y%m%d%H%M%S"))); -} - -struct LazyInput -{ - bool isFlake; - LockedInput lockedInput; -}; - -/* Helper primop to make callFlake (below) fetch/call its inputs - lazily. Note that this primop cannot be called by user code since - it doesn't appear in 'builtins'. */ -static void prim_callFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) -{ - auto lazyInput = (LazyInput *) args[0]->attrs; - - if (lazyInput->isFlake) { - FlakeCache flakeCache; - auto flake = getFlake(state, lazyInput->lockedInput.lockedRef, lazyInput->lockedInput.info, false, flakeCache); - - if (flake.sourceInfo->info.narHash != lazyInput->lockedInput.info.narHash) - throw Error("the content hash of flake '%s' (%s) doesn't match the hash recorded in the referring lock file (%s)", - lazyInput->lockedInput.lockedRef, - flake.sourceInfo->info.narHash.to_string(SRI), - lazyInput->lockedInput.info.narHash.to_string(SRI)); - - // FIXME: check all the other attributes in lockedInput.info - // once we've dropped support for lock file version 4. - - assert(flake.sourceInfo->storePath == lazyInput->lockedInput.computeStorePath(*state.store)); - - callFlake(state, flake, lazyInput->lockedInput, v); - } else { - FlakeCache flakeCache; - auto [sourceInfo, lockedRef] = fetchOrSubstituteTree( - state, lazyInput->lockedInput.lockedRef, {}, false, flakeCache); - - if (sourceInfo.info.narHash != lazyInput->lockedInput.info.narHash) - throw Error("the content hash of repository '%s' (%s) doesn't match the hash recorded in the referring lock file (%s)", - lazyInput->lockedInput.lockedRef, - sourceInfo.info.narHash.to_string(SRI), - lazyInput->lockedInput.info.narHash.to_string(SRI)); - - // FIXME: check all the other attributes in lockedInput.info - // once we've dropped support for lock file version 4. - - assert(sourceInfo.storePath == lazyInput->lockedInput.computeStorePath(*state.store)); - - state.mkAttrs(v, 8); - - assert(state.store->isValidPath(sourceInfo.storePath)); - - auto pathS = state.store->printStorePath(sourceInfo.storePath); - - mkString(*state.allocAttr(v, state.sOutPath), pathS, {pathS}); - - emitSourceInfoAttrs(state, lockedRef, sourceInfo, v); - - v.attrs->sort(); - } -} - void callFlake(EvalState & state, const Flake & flake, const LockedInputs & lockedInputs, - Value & vResFinal) + Value & vRes) { - auto & vRes = *state.allocValue(); - auto & vInputs = *state.allocValue(); + auto vCallFlake = state.allocValue(); + auto vLocks = state.allocValue(); + auto vRootSrc = state.allocValue(); + auto vTmp = state.allocValue(); - state.mkAttrs(vInputs, flake.inputs.size() + 1); + mkString(*vLocks, lockedInputs.to_string()); - for (auto & [inputId, input] : flake.inputs) { - auto vFlake = state.allocAttr(vInputs, inputId); - auto vPrimOp = state.allocValue(); - static auto primOp = new PrimOp(prim_callFlake, 1, state.symbols.create("callFlake")); - vPrimOp->type = tPrimOp; - vPrimOp->primOp = primOp; - auto vArg = state.allocValue(); - vArg->type = tNull; - auto lockedInput = lockedInputs.inputs.find(inputId); - assert(lockedInput != lockedInputs.inputs.end()); - // FIXME: leak - vArg->attrs = (Bindings *) new LazyInput{input.isFlake, lockedInput->second}; - mkApp(*vFlake, *vPrimOp, *vArg); - } + emitTreeAttrs(state, *flake.sourceInfo, flake.lockedRef.input, *vRootSrc); - auto & vSourceInfo = *state.allocValue(); - state.mkAttrs(vSourceInfo, 8); - emitSourceInfoAttrs(state, flake.lockedRef, *flake.sourceInfo, vSourceInfo); - vSourceInfo.attrs->sort(); - - vInputs.attrs->push_back(Attr(state.sSelf, &vRes)); - - vInputs.attrs->sort(); - - /* For convenience, put the outputs directly in the result, so you - can refer to an output of an input as 'inputs.foo.bar' rather - than 'inputs.foo.outputs.bar'. */ - auto vCall = *state.allocValue(); - state.eval(state.parseExprFromString( - "outputsFun: inputs: sourceInfo: let outputs = outputsFun inputs; in " - "outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; }", "/"), vCall); - - auto vCall2 = *state.allocValue(); - auto vCall3 = *state.allocValue(); - state.callFunction(vCall, *flake.vOutputs, vCall2, noPos); - state.callFunction(vCall2, vInputs, vCall3, noPos); - state.callFunction(vCall3, vSourceInfo, vRes, noPos); - - vResFinal = vRes; + state.evalFile(canonPath(settings.nixDataDir + "/nix/corepkgs/call-flake.nix", true), *vCallFlake); + state.callFunction(*vCallFlake, *vLocks, *vTmp, noPos); + state.callFunction(*vTmp, *vRootSrc, vRes, noPos); } void callFlake(EvalState & state, @@ -767,7 +644,6 @@ void callFlake(EvalState & state, callFlake(state, lockedFlake.flake, lockedFlake.lockFile, v); } -// This function is exposed to be used in nix files. static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) { callFlake(state, diff --git a/src/libexpr/flake/flake.hh b/src/libexpr/flake/flake.hh index 7c71f3383..e5c6c1bb0 100644 --- a/src/libexpr/flake/flake.hh +++ b/src/libexpr/flake/flake.hh @@ -107,4 +107,10 @@ void callFlake( } +void emitTreeAttrs( + EvalState & state, + const fetchers::Tree & tree, + std::shared_ptr input, + Value & v); + } diff --git a/src/libexpr/flake/lockfile.cc b/src/libexpr/flake/lockfile.cc index 0a1795a16..fdbba44de 100644 --- a/src/libexpr/flake/lockfile.cc +++ b/src/libexpr/flake/lockfile.cc @@ -123,6 +123,11 @@ nlohmann::json LockedInputs::toJson() const return json; } +std::string LockedInputs::to_string() const +{ + return toJson().dump(2); +} + bool LockedInputs::isImmutable() const { for (auto & i : inputs) diff --git a/src/libexpr/flake/lockfile.hh b/src/libexpr/flake/lockfile.hh index 51649df3d..82cbffd19 100644 --- a/src/libexpr/flake/lockfile.hh +++ b/src/libexpr/flake/lockfile.hh @@ -27,6 +27,8 @@ struct LockedInputs nlohmann::json toJson() const; + std::string to_string() const; + bool isImmutable() const; std::optional findInput(const InputPath & path); diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index c790b30f6..05d0792ef 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -20,6 +20,7 @@ struct RegisterPrimOp them. */ /* Load a ValueInitializer from a DSO and return whatever it initializes */ void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v); + /* Execute a program and parse its output */ void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v); diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 60bd2ed11..66994c823 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -4,11 +4,45 @@ #include "fetchers/fetchers.hh" #include "fetchers/registry.hh" +#include +#include + namespace nix { +void emitTreeAttrs( + EvalState & state, + const fetchers::Tree & tree, + std::shared_ptr input, + Value & v) +{ + state.mkAttrs(v, 8); + + auto storePath = state.store->printStorePath(tree.storePath); + + mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath})); + + assert(tree.info.narHash); + mkString(*state.allocAttr(v, state.symbols.create("narHash")), + tree.info.narHash.to_string(SRI)); + + if (input->getRev()) { + mkString(*state.allocAttr(v, state.symbols.create("rev")), input->getRev()->gitRev()); + mkString(*state.allocAttr(v, state.symbols.create("shortRev")), input->getRev()->gitShortRev()); + } + + if (tree.info.revCount) + mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount); + + if (tree.info.lastModified) + mkString(*state.allocAttr(v, state.symbols.create("lastModified")), + fmt("%s", std::put_time(std::gmtime(&*tree.info.lastModified), "%Y%m%d%H%M%S"))); + + v.attrs->sort(); +} + static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, Value & v) { - settings.requireExperimentalFeature("fetch-tree"); + settings.requireExperimentalFeature("flakes"); std::shared_ptr input; PathSet context; @@ -25,7 +59,8 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V if (attr.value->type == tString) attrs.emplace(attr.name, attr.value->string.s); else - throw Error("unsupported attribute type"); + throw TypeError("fetchTree argument '%s' is %s while a string is expected", + attr.name, showType(*attr.value)); } if (!attrs.count("type")) @@ -43,19 +78,10 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V auto [tree, input2] = input->fetchTree(state.store); - state.mkAttrs(v, 8); - auto storePath = state.store->printStorePath(tree.storePath); - mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath})); - if (input2->getRev()) { - mkString(*state.allocAttr(v, state.symbols.create("rev")), input2->getRev()->gitRev()); - mkString(*state.allocAttr(v, state.symbols.create("shortRev")), input2->getRev()->gitShortRev()); - } - if (tree.info.revCount) - mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount); - v.attrs->sort(); - if (state.allowedPaths) state.allowedPaths->insert(tree.actualPath); + + emitTreeAttrs(state, tree, input2, v); } static RegisterPrimOp r("fetchTree", 1, prim_fetchTree); diff --git a/tests/tarball.sh b/tests/tarball.sh index 55ed3e318..b3ec16d40 100644 --- a/tests/tarball.sh +++ b/tests/tarball.sh @@ -27,10 +27,10 @@ test_tarball() { nix-build -o $TEST_ROOT/result -E "import (fetchTarball file://$tarball)" - nix-build --experimental-features fetch-tree -o $TEST_ROOT/result -E "import (fetchTree file://$tarball)" - nix-build --experimental-features fetch-tree -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; })" - nix-build --experimental-features fetch-tree -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })" - nix-build --experimental-features fetch-tree -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" 2>&1 | grep 'NAR hash mismatch in input' + nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree file://$tarball)" + nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; })" + nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })" + nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" 2>&1 | grep 'NAR hash mismatch in input' nix-instantiate --eval -E '1 + 2' -I fnord=file://no-such-tarball.tar$ext nix-instantiate --eval -E 'with ; 1 + 2' -I fnord=file://no-such-tarball$ext