diff --git a/flake.lock b/flake.lock index 75c78d102..46e6c9dc5 100644 --- a/flake.lock +++ b/flake.lock @@ -2,9 +2,9 @@ "inputs": { "nixpkgs": { "inputs": {}, - "narHash": "sha256-HkMF+s/yqNOOxqZGp+rscaC8LPtOGc50nEAjLFsnJpg=", + "narHash": "sha256-V4jz8Hbt+mZkXhH+3KmUQcRGETOFd8mVPhgQlS4Lu5E=", "originalUrl": "flake:nixpkgs/release-19.09", - "url": "github:edolstra/nixpkgs/e7223c602152ee4544b05157fc9d88a3feed22c2" + "url": "github:edolstra/nixpkgs/dd45a16733f4469a0dded6ad0bf9a662ea39bdea" } }, "version": 3 diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 2982f7718..adcb88dcc 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -251,17 +251,6 @@ static std::pair getNonFlake( return std::make_pair(std::move(sourceInfo), resolvedRef); } -bool allowedToUseRegistries(LockFileMode handle, bool isTopRef) -{ - if (handle == AllPure) return false; - else if (handle == TopRefUsesRegistries) return isTopRef; - else if (handle == UpdateLockFile) return true; - else if (handle == UseUpdatedLockFile) return true; - else if (handle == RecreateLockFile) return true; - else if (handle == UseNewLockFile) return true; - else assert(false); -} - static void flattenLockFile( const LockedInputs & inputs, const InputPath & prefix, @@ -311,20 +300,17 @@ static std::string diffLockFiles(const LockedInputs & oldLocks, const LockedInpu LockedFlake lockFlake( EvalState & state, const FlakeRef & topRef, - LockFileMode lockFileMode, const LockFlags & lockFlags) { settings.requireExperimentalFeature("flakes"); RefMap refMap; - auto flake = getFlake(state, topRef, - allowedToUseRegistries(lockFileMode, true), refMap); + auto flake = getFlake(state, topRef, lockFlags.useRegistries, refMap); LockFile oldLockFile; - if (lockFileMode != RecreateLockFile && lockFileMode != UseNewLockFile) { - // If recreateLockFile, start with an empty lockfile + if (!lockFlags.recreateLockFile) { // FIXME: symlink attack oldLockFile = LockFile::read( flake.sourceInfo->actualPath + "/" + flake.resolvedRef.subdir + "/flake.lock"); @@ -436,12 +422,13 @@ LockedFlake lockFlake( } else { /* We need to update/create a new lock file entry. So fetch the flake/non-flake. */ - if (lockFileMode == AllPure || lockFileMode == TopRefUsesRegistries) + + if (!lockFlags.allowMutable && !input.ref.isImmutable()) throw Error("cannot update flake input '%s' in pure mode", inputPathS); if (input.isFlake) { auto inputFlake = getFlake(state, input.ref, - allowedToUseRegistries(lockFileMode, false), refMap); + lockFlags.useRegistries, refMap); newLocks.inputs.insert_or_assign(id, LockedInput(inputFlake.resolvedRef, inputFlake.originalRef, inputFlake.sourceInfo->narHash)); @@ -461,7 +448,7 @@ LockedFlake lockFlake( else { auto [sourceInfo, resolvedRef] = getNonFlake(state, input.ref, - allowedToUseRegistries(lockFileMode, false), refMap); + lockFlags.useRegistries, refMap); newLocks.inputs.insert_or_assign(id, LockedInput(resolvedRef, input.ref, sourceInfo.narHash)); } @@ -494,12 +481,15 @@ LockedFlake lockFlake( if (!(oldLockFile == LockFile())) printInfo("inputs of flake '%s' changed:\n%s", topRef, chomp(diffLockFiles(oldLockFile, newLockFile))); - if (lockFileMode == UpdateLockFile || lockFileMode == RecreateLockFile) { + if (lockFlags.writeLockFile) { if (auto sourcePath = topRef.input->getSourcePath()) { if (!newLockFile.isImmutable()) { if (settings.warnDirty) warn("will not write lock file of flake '%s' because it has a mutable input", topRef); } else { + if (!lockFlags.updateLockFile) + throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef); + auto path = *sourcePath + (topRef.subdir == "" ? "" : "/" + topRef.subdir) + "/flake.lock"; if (pathExists(path)) @@ -522,9 +512,9 @@ LockedFlake lockFlake( #endif } } else - warn("cannot write lock file of remote flake '%s'", topRef); - } else if (lockFileMode != AllPure && lockFileMode != TopRefUsesRegistries) - warn("using updated lock file without writing it to file"); + 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'", topRef); } return LockedFlake { .flake = std::move(flake), .lockFile = std::move(newLockFile) }; @@ -655,9 +645,14 @@ void callFlake(EvalState & state, // This function is exposed to be used in nix files. static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) { - LockFlags lockFlags; - callFlake(state, lockFlake(state, parseFlakeRef(state.forceStringNoCtx(*args[0], pos)), - evalSettings.pureEval ? AllPure : UseUpdatedLockFile, lockFlags), v); + callFlake(state, + lockFlake(state, parseFlakeRef(state.forceStringNoCtx(*args[0], pos)), + LockFlags { + .updateLockFile = false, + .useRegistries = !evalSettings.pureEval, + .allowMutable = !evalSettings.pureEval, + }), + v); } static RegisterPrimOp r2("getFlake", 1, prim_getFlake); diff --git a/src/libexpr/flake/flake.hh b/src/libexpr/flake/flake.hh index eb013c4ba..355e22adc 100644 --- a/src/libexpr/flake/flake.hh +++ b/src/libexpr/flake/flake.hh @@ -13,15 +13,6 @@ namespace fetchers { struct Tree; } namespace flake { -enum LockFileMode : unsigned int - { AllPure // Everything is handled 100% purely - , TopRefUsesRegistries // The top FlakeRef uses the registries, apart from that, everything happens 100% purely - , UpdateLockFile // Update the existing lockfile and write it to file - , UseUpdatedLockFile // `UpdateLockFile` without writing to file - , RecreateLockFile // Recreate the lockfile from scratch and write it to file - , UseNewLockFile // `RecreateLockFile` without writing to file - }; - struct FlakeInput; typedef std::map FlakeInputs; @@ -61,21 +52,48 @@ struct LockedFlake struct LockFlags { + /* Whether to ignore the existing lock file, creating a new one + from scratch. */ + bool recreateLockFile = false; + + /* Whether to update the lock file at all. If set to false, if any + change to the lock file is needed (e.g. when an input has been + added to flake.nix), you get a fatal error. */ + bool updateLockFile = true; + + /* Whether to write the lock file to disk. If set to true, if the + any changes to the lock file are needed and the flake is not + writable (i.e. is not a local Git working tree or similar), you + get a fatal error. If set to false, Nix will use the modified + lock file in memory only, without writing it to disk. */ + bool writeLockFile = true; + + /* Whether to use the registries to lookup indirect flake + references like 'nixpkgs'. */ + bool useRegistries = true; + + /* Whether mutable flake references (i.e. those without a Git + revision or similar) without a corresponding lock are + allowed. Mutable flake references with a lock are always + allowed. */ + bool allowMutable = true; + std::map inputOverrides; }; LockedFlake lockFlake( - EvalState &, - const FlakeRef &, - LockFileMode, - const LockFlags &); + EvalState & state, + const FlakeRef & flakeRef, + const LockFlags & lockFlags); -void callFlake(EvalState & state, +void callFlake( + EvalState & state, const Flake & flake, const LockedInputs & inputs, Value & v); -void callFlake(EvalState & state, +void callFlake( + EvalState & state, const LockedFlake & resFlake, Value & v); diff --git a/src/nix/command.hh b/src/nix/command.hh index eb44caf05..305ce5588 100644 --- a/src/nix/command.hh +++ b/src/nix/command.hh @@ -16,10 +16,6 @@ class EvalState; struct Pos; class Store; -namespace flake { -enum LockFileMode : unsigned int; -} - /* A command that requires a Nix store. */ struct StoreCommand : virtual Command { @@ -42,15 +38,9 @@ struct EvalCommand : virtual StoreCommand, MixEvalArgs struct MixFlakeOptions : virtual Args { - bool recreateLockFile = false; - bool saveLockFile = true; - bool useRegistries = true; - flake::LockFlags lockFlags; MixFlakeOptions(); - - flake::LockFileMode getLockFileMode(); }; struct SourceExprCommand : virtual Args, EvalCommand, MixFlakeOptions diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 2852a627d..49c0d30f0 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -38,12 +38,12 @@ public: Flake getFlake() { auto evalState = getEvalState(); - return flake::getFlake(*evalState, getFlakeRef(), useRegistries); + return flake::getFlake(*evalState, getFlakeRef(), lockFlags.useRegistries); } LockedFlake lockFlake() { - return flake::lockFlake(*getEvalState(), getFlakeRef(), getLockFileMode(), lockFlags); + return flake::lockFlake(*getEvalState(), getFlakeRef(), lockFlags); } }; diff --git a/src/nix/installables.cc b/src/nix/installables.cc index 7d59a25ee..24eb739f5 100644 --- a/src/nix/installables.cc +++ b/src/nix/installables.cc @@ -22,17 +22,22 @@ MixFlakeOptions::MixFlakeOptions() mkFlag() .longName("recreate-lock-file") .description("recreate lock file from scratch") - .set(&recreateLockFile, true); + .set(&lockFlags.recreateLockFile, true); mkFlag() - .longName("no-save-lock-file") - .description("do not save the newly generated lock file") - .set(&saveLockFile, false); + .longName("no-update-lock-file") + .description("do not allow any updates to the lock file") + .set(&lockFlags.updateLockFile, false); + + mkFlag() + .longName("no-write-lock-file") + .description("do not write the newly generated lock file") + .set(&lockFlags.writeLockFile, false); mkFlag() .longName("no-registries") .description("don't use flake registries") - .set(&useRegistries, false); + .set(&lockFlags.useRegistries, false); mkFlag() .longName("override-input") @@ -46,17 +51,6 @@ MixFlakeOptions::MixFlakeOptions() }); } -flake::LockFileMode MixFlakeOptions::getLockFileMode() -{ - using namespace flake; - return - useRegistries - ? recreateLockFile - ? (saveLockFile ? RecreateLockFile : UseNewLockFile) - : (saveLockFile ? UpdateLockFile : UseUpdatedLockFile) - : AllPure; -} - SourceExprCommand::SourceExprCommand() { mkFlag() @@ -332,7 +326,7 @@ std::tuple InstallableFlake { auto state = cmd.getEvalState(); - auto lockedFlake = lockFlake(*state, flakeRef, cmd.getLockFileMode(), cmd.lockFlags); + auto lockedFlake = lockFlake(*state, flakeRef, cmd.lockFlags); Value * vOutputs = nullptr; @@ -386,7 +380,7 @@ std::vector InstallableFlake::toDerivations() Value * InstallableFlake::toValue(EvalState & state) { - auto lockedFlake = lockFlake(state, flakeRef, cmd.getLockFileMode(), cmd.lockFlags); + auto lockedFlake = lockFlake(state, flakeRef, cmd.lockFlags); auto vOutputs = getFlakeOutputs(state, lockedFlake); diff --git a/tests/flakes.sh b/tests/flakes.sh index 1b1912129..3eae73cdf 100644 --- a/tests/flakes.sh +++ b/tests/flakes.sh @@ -156,9 +156,15 @@ nix path-info $flake1Dir/result (! 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) +nix build -o $TEST_ROOT/result flake2#bar --impure --no-write-lock-file -# Test automatic lock file generation. +# Building a local flake with an unlocked dependency should fail with --no-update-lock-file. +nix build -o $TEST_ROOT/result $flake2Dir#bar --no-update-lock-file 2>&1 | grep 'requires lock file changes' + +# But it should succeed without that flag. +nix build -o $TEST_ROOT/result $flake2Dir#bar --no-write-lock-file +nix build -o $TEST_ROOT/result $flake2Dir#bar --no-update-lock-file 2>&1 | grep 'requires lock file changes' nix build -o $TEST_ROOT/result $flake2Dir#bar [[ -e $flake2Dir/flake.lock ]] git -C $flake2Dir add flake.lock