mirror of
https://github.com/NixOS/nix
synced 2025-06-25 02:21:16 +02:00
Add setting 'allow-dirty-locks'
This allows writing lock files with dirty inputs, so long as they have a NAR hash. (Currently they always have a NAR hash, but with lazy trees that may not always be the case.) Generally dirty locks are bad for reproducibility (we can detect if the dirty input has changed, but we have no way to fetch it except substitution). Hence we don't allow them by default. Fixes #11181.
This commit is contained in:
parent
2d9b213cc2
commit
e161393299
12 changed files with 67 additions and 17 deletions
|
@ -450,7 +450,7 @@ ref<eval_cache::EvalCache> openEvalCache(
|
|||
std::shared_ptr<flake::LockedFlake> lockedFlake)
|
||||
{
|
||||
auto fingerprint = evalSettings.useEvalCache && evalSettings.pureEval
|
||||
? lockedFlake->getFingerprint(state.store)
|
||||
? lockedFlake->getFingerprint(state.store, state.fetchSettings)
|
||||
: std::nullopt;
|
||||
auto rootLoader = [&state, lockedFlake]()
|
||||
{
|
||||
|
|
|
@ -182,7 +182,7 @@ static void fetchTree(
|
|||
if (!state.settings.pureEval && !input.isDirect() && experimentalFeatureSettings.isEnabled(Xp::Flakes))
|
||||
input = lookupInRegistries(state.store, input).first;
|
||||
|
||||
if (state.settings.pureEval && !input.isLocked()) {
|
||||
if (state.settings.pureEval && !input.isConsideredLocked(state.fetchSettings)) {
|
||||
auto fetcher = "fetchTree";
|
||||
if (params.isFetchGit)
|
||||
fetcher = "fetchGit";
|
||||
|
|
|
@ -70,6 +70,22 @@ struct Settings : public Config
|
|||
Setting<bool> warnDirty{this, true, "warn-dirty",
|
||||
"Whether to warn about dirty Git/Mercurial trees."};
|
||||
|
||||
Setting<bool> allowDirtyLocks{
|
||||
this,
|
||||
false,
|
||||
"allow-dirty-locks",
|
||||
R"(
|
||||
Whether to allow dirty inputs (such as dirty Git workdirs)
|
||||
to be locked via their NAR hash. This is generally bad
|
||||
practice since Nix has no way to obtain such inputs if they
|
||||
are subsequently modified. Therefore lock files with dirty
|
||||
locks should generally only be used for local testing, and
|
||||
should not be pushed to other users.
|
||||
)",
|
||||
{},
|
||||
true,
|
||||
Xp::Flakes};
|
||||
|
||||
Setting<bool> trustTarballsFromGitForges{
|
||||
this, true, "trust-tarballs-from-git-forges",
|
||||
R"(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "fetch-to-store.hh"
|
||||
#include "json-utils.hh"
|
||||
#include "store-path-accessor.hh"
|
||||
#include "fetch-settings.hh"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
|
@ -154,6 +155,12 @@ bool Input::isLocked() const
|
|||
return scheme && scheme->isLocked(*this);
|
||||
}
|
||||
|
||||
bool Input::isConsideredLocked(
|
||||
const Settings & settings) const
|
||||
{
|
||||
return isLocked() || (settings.allowDirtyLocks && getNarHash());
|
||||
}
|
||||
|
||||
bool Input::isFinal() const
|
||||
{
|
||||
return maybeGetBoolAttr(attrs, "__final").value_or(false);
|
||||
|
|
|
@ -95,6 +95,15 @@ public:
|
|||
*/
|
||||
bool isLocked() const;
|
||||
|
||||
/**
|
||||
* Return whether the input is either locked, or, if
|
||||
* `allow-dirty-locks` is enabled, it has a NAR hash. In the
|
||||
* latter case, we can verify the input but we may not be able to
|
||||
* fetch it from anywhere.
|
||||
*/
|
||||
bool isConsideredLocked(
|
||||
const Settings & settings) const;
|
||||
|
||||
/**
|
||||
* Return whether this is a "final" input, meaning that fetching
|
||||
* it will not add, remove or change any attributes. (See
|
||||
|
|
|
@ -345,6 +345,7 @@ Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup
|
|||
}
|
||||
|
||||
static LockFile readLockFile(
|
||||
const Settings & settings,
|
||||
const fetchers::Settings & fetchSettings,
|
||||
const SourcePath & lockFilePath)
|
||||
{
|
||||
|
@ -380,6 +381,7 @@ LockedFlake lockFlake(
|
|||
}
|
||||
|
||||
auto oldLockFile = readLockFile(
|
||||
settings,
|
||||
state.fetchSettings,
|
||||
lockFlags.referenceLockFilePath.value_or(
|
||||
flake.lockFilePath()));
|
||||
|
@ -616,7 +618,7 @@ LockedFlake lockFlake(
|
|||
inputFlake.inputs, childNode, inputPath,
|
||||
oldLock
|
||||
? std::dynamic_pointer_cast<const Node>(oldLock)
|
||||
: readLockFile(state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(),
|
||||
: readLockFile(settings, state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(),
|
||||
oldLock ? lockRootPath : inputPath,
|
||||
localPath,
|
||||
false);
|
||||
|
@ -678,9 +680,11 @@ LockedFlake lockFlake(
|
|||
|
||||
if (lockFlags.writeLockFile) {
|
||||
if (sourcePath || lockFlags.outputLockFilePath) {
|
||||
if (auto unlockedInput = newLockFile.isUnlocked()) {
|
||||
if (auto unlockedInput = newLockFile.isUnlocked(state.fetchSettings)) {
|
||||
if (lockFlags.failOnUnlocked)
|
||||
throw Error("cannot write lock file of flake '%s' because it has an unlocked input ('%s').\n", topRef, *unlockedInput);
|
||||
throw Error(
|
||||
"Will not write lock file of flake '%s' because it has an unlocked input ('%s'). "
|
||||
"Use '--allow-dirty-locks' to allow this anyway.", topRef, *unlockedInput);
|
||||
if (state.fetchSettings.warnDirty)
|
||||
warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput);
|
||||
} else {
|
||||
|
@ -979,9 +983,11 @@ static RegisterPrimOp r4({
|
|||
|
||||
}
|
||||
|
||||
std::optional<Fingerprint> LockedFlake::getFingerprint(ref<Store> store) const
|
||||
std::optional<Fingerprint> LockedFlake::getFingerprint(
|
||||
ref<Store> store,
|
||||
const fetchers::Settings & fetchSettings) const
|
||||
{
|
||||
if (lockFile.isUnlocked()) return std::nullopt;
|
||||
if (lockFile.isUnlocked(fetchSettings)) return std::nullopt;
|
||||
|
||||
auto fingerprint = flake.lockedRef.input.getFingerprint(store);
|
||||
if (!fingerprint) return std::nullopt;
|
||||
|
|
|
@ -129,7 +129,9 @@ struct LockedFlake
|
|||
*/
|
||||
std::map<ref<Node>, SourcePath> nodePaths;
|
||||
|
||||
std::optional<Fingerprint> getFingerprint(ref<Store> store) const;
|
||||
std::optional<Fingerprint> getFingerprint(
|
||||
ref<Store> store,
|
||||
const fetchers::Settings & fetchSettings) const;
|
||||
};
|
||||
|
||||
struct LockFlags
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "strings.hh"
|
||||
#include "flake/settings.hh"
|
||||
|
||||
namespace nix::flake {
|
||||
|
||||
|
@ -43,8 +44,8 @@ LockedNode::LockedNode(
|
|||
, originalRef(getFlakeRef(fetchSettings, json, "original", nullptr))
|
||||
, isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true)
|
||||
{
|
||||
if (!lockedRef.input.isLocked())
|
||||
throw Error("lock file contains unlocked input '%s'",
|
||||
if (!lockedRef.input.isConsideredLocked(fetchSettings))
|
||||
throw Error("Lock file contains unlocked input '%s'. Use '--allow-dirty-locks' to accept this lock file.",
|
||||
fetchers::attrsToJSON(lockedRef.input.toAttrs()));
|
||||
|
||||
// For backward compatibility, lock file entries are implicitly final.
|
||||
|
@ -228,7 +229,7 @@ std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile)
|
|||
return stream;
|
||||
}
|
||||
|
||||
std::optional<FlakeRef> LockFile::isUnlocked() const
|
||||
std::optional<FlakeRef> LockFile::isUnlocked(const fetchers::Settings & fetchSettings) const
|
||||
{
|
||||
std::set<ref<const Node>> nodes;
|
||||
|
||||
|
@ -247,7 +248,7 @@ std::optional<FlakeRef> LockFile::isUnlocked() const
|
|||
for (auto & i : nodes) {
|
||||
if (i == ref<const Node>(root)) continue;
|
||||
auto node = i.dynamic_pointer_cast<const LockedNode>();
|
||||
if (node && (!node->lockedRef.input.isLocked() || !node->lockedRef.input.isFinal()))
|
||||
if (node && (!node->lockedRef.input.isConsideredLocked(fetchSettings) || !node->lockedRef.input.isFinal()))
|
||||
return node->lockedRef;
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ struct LockFile
|
|||
* Check whether this lock file has any unlocked or non-final
|
||||
* inputs. If so, return one.
|
||||
*/
|
||||
std::optional<FlakeRef> isUnlocked() const;
|
||||
std::optional<FlakeRef> isUnlocked(const fetchers::Settings & fetchSettings) const;
|
||||
|
||||
bool operator ==(const LockFile & other) const;
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ struct Settings : public Config
|
|||
this,
|
||||
false,
|
||||
"accept-flake-config",
|
||||
"Whether to accept nix configuration from a flake without prompting.",
|
||||
"Whether to accept Nix configuration settings from a flake without prompting.",
|
||||
{},
|
||||
true,
|
||||
Xp::Flakes};
|
||||
|
|
|
@ -238,7 +238,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
|||
j["lastModified"] = *lastModified;
|
||||
j["path"] = storePath;
|
||||
j["locks"] = lockedFlake.lockFile.toJSON().first;
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store))
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings))
|
||||
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
|
||||
logger->cout("%s", j.dump());
|
||||
} else {
|
||||
|
@ -272,7 +272,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
|||
logger->cout(
|
||||
ANSI_BOLD "Last modified:" ANSI_NORMAL " %s",
|
||||
std::put_time(std::localtime(&*lastModified), "%F %T"));
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store))
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings))
|
||||
logger->cout(
|
||||
ANSI_BOLD "Fingerprint:" ANSI_NORMAL " %s",
|
||||
fingerprint->to_string(HashFormat::Base16, false));
|
||||
|
|
|
@ -31,5 +31,14 @@ echo 456 > "$flake1Dir"/x.nix
|
|||
|
||||
[[ $(nix eval --json "$flake2Dir#x" --override-input flake1 "$TEST_ROOT/flake1") = 456 ]]
|
||||
|
||||
# Dirty overrides require --allow-dirty-locks.
|
||||
expectStderr 1 nix flake lock "$flake2Dir" --override-input flake1 "$TEST_ROOT/flake1" |
|
||||
grepQuiet "cannot write lock file.*because it has an unlocked input"
|
||||
grepQuiet "Will not write lock file.*because it has an unlocked input"
|
||||
|
||||
nix flake lock "$flake2Dir" --override-input flake1 "$TEST_ROOT/flake1" --allow-dirty-locks
|
||||
|
||||
# Using a lock file with a dirty lock requires --allow-dirty-locks as well.
|
||||
expectStderr 1 nix eval "$flake2Dir#x" |
|
||||
grepQuiet "Lock file contains unlocked input"
|
||||
|
||||
[[ $(nix eval "$flake2Dir#x" --allow-dirty-locks) = 456 ]]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue