Clean up the lock file handling flags

Added a flag --no-update-lock-file to barf if the lock file needs any
changes. This is useful for CI systems if you're building a
checkout. Fixes #2947.

Renamed --no-save-lock-file to --no-write-lock-file. It is now a fatal
error if the lock file needs changes but --no-write-lock-file is not
given.
This commit is contained in:
Eelco Dolstra 2020-01-29 21:01:34 +01:00
parent f68bed7f67
commit 26f895a26d
7 changed files with 78 additions and 75 deletions

View file

@ -2,9 +2,9 @@
"inputs": { "inputs": {
"nixpkgs": { "nixpkgs": {
"inputs": {}, "inputs": {},
"narHash": "sha256-HkMF+s/yqNOOxqZGp+rscaC8LPtOGc50nEAjLFsnJpg=", "narHash": "sha256-V4jz8Hbt+mZkXhH+3KmUQcRGETOFd8mVPhgQlS4Lu5E=",
"originalUrl": "flake:nixpkgs/release-19.09", "originalUrl": "flake:nixpkgs/release-19.09",
"url": "github:edolstra/nixpkgs/e7223c602152ee4544b05157fc9d88a3feed22c2" "url": "github:edolstra/nixpkgs/dd45a16733f4469a0dded6ad0bf9a662ea39bdea"
} }
}, },
"version": 3 "version": 3

View file

@ -251,17 +251,6 @@ static std::pair<fetchers::Tree, FlakeRef> getNonFlake(
return std::make_pair(std::move(sourceInfo), resolvedRef); 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( static void flattenLockFile(
const LockedInputs & inputs, const LockedInputs & inputs,
const InputPath & prefix, const InputPath & prefix,
@ -311,20 +300,17 @@ static std::string diffLockFiles(const LockedInputs & oldLocks, const LockedInpu
LockedFlake lockFlake( LockedFlake lockFlake(
EvalState & state, EvalState & state,
const FlakeRef & topRef, const FlakeRef & topRef,
LockFileMode lockFileMode,
const LockFlags & lockFlags) const LockFlags & lockFlags)
{ {
settings.requireExperimentalFeature("flakes"); settings.requireExperimentalFeature("flakes");
RefMap refMap; RefMap refMap;
auto flake = getFlake(state, topRef, auto flake = getFlake(state, topRef, lockFlags.useRegistries, refMap);
allowedToUseRegistries(lockFileMode, true), refMap);
LockFile oldLockFile; LockFile oldLockFile;
if (lockFileMode != RecreateLockFile && lockFileMode != UseNewLockFile) { if (!lockFlags.recreateLockFile) {
// If recreateLockFile, start with an empty lockfile
// FIXME: symlink attack // FIXME: symlink attack
oldLockFile = LockFile::read( oldLockFile = LockFile::read(
flake.sourceInfo->actualPath + "/" + flake.resolvedRef.subdir + "/flake.lock"); flake.sourceInfo->actualPath + "/" + flake.resolvedRef.subdir + "/flake.lock");
@ -436,12 +422,13 @@ LockedFlake lockFlake(
} else { } else {
/* We need to update/create a new lock file /* We need to update/create a new lock file
entry. So fetch the flake/non-flake. */ 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); throw Error("cannot update flake input '%s' in pure mode", inputPathS);
if (input.isFlake) { if (input.isFlake) {
auto inputFlake = getFlake(state, input.ref, auto inputFlake = getFlake(state, input.ref,
allowedToUseRegistries(lockFileMode, false), refMap); lockFlags.useRegistries, refMap);
newLocks.inputs.insert_or_assign(id, newLocks.inputs.insert_or_assign(id,
LockedInput(inputFlake.resolvedRef, inputFlake.originalRef, inputFlake.sourceInfo->narHash)); LockedInput(inputFlake.resolvedRef, inputFlake.originalRef, inputFlake.sourceInfo->narHash));
@ -461,7 +448,7 @@ LockedFlake lockFlake(
else { else {
auto [sourceInfo, resolvedRef] = getNonFlake(state, input.ref, auto [sourceInfo, resolvedRef] = getNonFlake(state, input.ref,
allowedToUseRegistries(lockFileMode, false), refMap); lockFlags.useRegistries, refMap);
newLocks.inputs.insert_or_assign(id, newLocks.inputs.insert_or_assign(id,
LockedInput(resolvedRef, input.ref, sourceInfo.narHash)); LockedInput(resolvedRef, input.ref, sourceInfo.narHash));
} }
@ -494,12 +481,15 @@ LockedFlake lockFlake(
if (!(oldLockFile == LockFile())) if (!(oldLockFile == LockFile()))
printInfo("inputs of flake '%s' changed:\n%s", topRef, chomp(diffLockFiles(oldLockFile, newLockFile))); 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 (auto sourcePath = topRef.input->getSourcePath()) {
if (!newLockFile.isImmutable()) { if (!newLockFile.isImmutable()) {
if (settings.warnDirty) if (settings.warnDirty)
warn("will not write lock file of flake '%s' because it has a mutable input", topRef); warn("will not write lock file of flake '%s' because it has a mutable input", topRef);
} else { } 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"; auto path = *sourcePath + (topRef.subdir == "" ? "" : "/" + topRef.subdir) + "/flake.lock";
if (pathExists(path)) if (pathExists(path))
@ -522,9 +512,9 @@ LockedFlake lockFlake(
#endif #endif
} }
} else } else
warn("cannot write lock file of remote flake '%s'", topRef); throw Error("cannot write modified lock file of flake '%s' (use '--no-write-lock-file' to ignore)", topRef);
} else if (lockFileMode != AllPure && lockFileMode != TopRefUsesRegistries) } else
warn("using updated lock file without writing it to file"); warn("not writing modified lock file of flake '%s'", topRef);
} }
return LockedFlake { .flake = std::move(flake), .lockFile = std::move(newLockFile) }; 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. // This function is exposed to be used in nix files.
static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Value & v)
{ {
LockFlags lockFlags; callFlake(state,
callFlake(state, lockFlake(state, parseFlakeRef(state.forceStringNoCtx(*args[0], pos)), lockFlake(state, parseFlakeRef(state.forceStringNoCtx(*args[0], pos)),
evalSettings.pureEval ? AllPure : UseUpdatedLockFile, lockFlags), v); LockFlags {
.updateLockFile = false,
.useRegistries = !evalSettings.pureEval,
.allowMutable = !evalSettings.pureEval,
}),
v);
} }
static RegisterPrimOp r2("getFlake", 1, prim_getFlake); static RegisterPrimOp r2("getFlake", 1, prim_getFlake);

View file

@ -13,15 +13,6 @@ namespace fetchers { struct Tree; }
namespace flake { 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; struct FlakeInput;
typedef std::map<FlakeId, FlakeInput> FlakeInputs; typedef std::map<FlakeId, FlakeInput> FlakeInputs;
@ -61,21 +52,48 @@ struct LockedFlake
struct LockFlags 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<InputPath, FlakeRef> inputOverrides; std::map<InputPath, FlakeRef> inputOverrides;
}; };
LockedFlake lockFlake( LockedFlake lockFlake(
EvalState &, EvalState & state,
const FlakeRef &, const FlakeRef & flakeRef,
LockFileMode, const LockFlags & lockFlags);
const LockFlags &);
void callFlake(EvalState & state, void callFlake(
EvalState & state,
const Flake & flake, const Flake & flake,
const LockedInputs & inputs, const LockedInputs & inputs,
Value & v); Value & v);
void callFlake(EvalState & state, void callFlake(
EvalState & state,
const LockedFlake & resFlake, const LockedFlake & resFlake,
Value & v); Value & v);

View file

@ -16,10 +16,6 @@ class EvalState;
struct Pos; struct Pos;
class Store; class Store;
namespace flake {
enum LockFileMode : unsigned int;
}
/* A command that requires a Nix store. */ /* A command that requires a Nix store. */
struct StoreCommand : virtual Command struct StoreCommand : virtual Command
{ {
@ -42,15 +38,9 @@ struct EvalCommand : virtual StoreCommand, MixEvalArgs
struct MixFlakeOptions : virtual Args struct MixFlakeOptions : virtual Args
{ {
bool recreateLockFile = false;
bool saveLockFile = true;
bool useRegistries = true;
flake::LockFlags lockFlags; flake::LockFlags lockFlags;
MixFlakeOptions(); MixFlakeOptions();
flake::LockFileMode getLockFileMode();
}; };
struct SourceExprCommand : virtual Args, EvalCommand, MixFlakeOptions struct SourceExprCommand : virtual Args, EvalCommand, MixFlakeOptions

View file

@ -38,12 +38,12 @@ public:
Flake getFlake() Flake getFlake()
{ {
auto evalState = getEvalState(); auto evalState = getEvalState();
return flake::getFlake(*evalState, getFlakeRef(), useRegistries); return flake::getFlake(*evalState, getFlakeRef(), lockFlags.useRegistries);
} }
LockedFlake lockFlake() LockedFlake lockFlake()
{ {
return flake::lockFlake(*getEvalState(), getFlakeRef(), getLockFileMode(), lockFlags); return flake::lockFlake(*getEvalState(), getFlakeRef(), lockFlags);
} }
}; };

View file

@ -22,17 +22,22 @@ MixFlakeOptions::MixFlakeOptions()
mkFlag() mkFlag()
.longName("recreate-lock-file") .longName("recreate-lock-file")
.description("recreate lock file from scratch") .description("recreate lock file from scratch")
.set(&recreateLockFile, true); .set(&lockFlags.recreateLockFile, true);
mkFlag() mkFlag()
.longName("no-save-lock-file") .longName("no-update-lock-file")
.description("do not save the newly generated lock file") .description("do not allow any updates to the lock file")
.set(&saveLockFile, false); .set(&lockFlags.updateLockFile, false);
mkFlag()
.longName("no-write-lock-file")
.description("do not write the newly generated lock file")
.set(&lockFlags.writeLockFile, false);
mkFlag() mkFlag()
.longName("no-registries") .longName("no-registries")
.description("don't use flake registries") .description("don't use flake registries")
.set(&useRegistries, false); .set(&lockFlags.useRegistries, false);
mkFlag() mkFlag()
.longName("override-input") .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() SourceExprCommand::SourceExprCommand()
{ {
mkFlag() mkFlag()
@ -332,7 +326,7 @@ std::tuple<std::string, FlakeRef, flake::EvalCache::Derivation> InstallableFlake
{ {
auto state = cmd.getEvalState(); auto state = cmd.getEvalState();
auto lockedFlake = lockFlake(*state, flakeRef, cmd.getLockFileMode(), cmd.lockFlags); auto lockedFlake = lockFlake(*state, flakeRef, cmd.lockFlags);
Value * vOutputs = nullptr; Value * vOutputs = nullptr;
@ -386,7 +380,7 @@ std::vector<flake::EvalCache::Derivation> InstallableFlake::toDerivations()
Value * InstallableFlake::toValue(EvalState & state) 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); auto vOutputs = getFlakeOutputs(state, lockedFlake);

View file

@ -156,9 +156,15 @@ nix path-info $flake1Dir/result
(! nix eval --expr "builtins.getFlake \"$flake2Dir\"") (! nix eval --expr "builtins.getFlake \"$flake2Dir\"")
# But should succeed in impure mode. # 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 nix build -o $TEST_ROOT/result $flake2Dir#bar
[[ -e $flake2Dir/flake.lock ]] [[ -e $flake2Dir/flake.lock ]]
git -C $flake2Dir add flake.lock git -C $flake2Dir add flake.lock