Skip to content

Commit e161393

Browse files
committed
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.
1 parent 2d9b213 commit e161393

File tree

12 files changed

+67
-17
lines changed

12 files changed

+67
-17
lines changed

src/libcmd/installables.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ ref<eval_cache::EvalCache> openEvalCache(
450450
std::shared_ptr<flake::LockedFlake> lockedFlake)
451451
{
452452
auto fingerprint = evalSettings.useEvalCache && evalSettings.pureEval
453-
? lockedFlake->getFingerprint(state.store)
453+
? lockedFlake->getFingerprint(state.store, state.fetchSettings)
454454
: std::nullopt;
455455
auto rootLoader = [&state, lockedFlake]()
456456
{

src/libexpr/primops/fetchTree.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ static void fetchTree(
182182
if (!state.settings.pureEval && !input.isDirect() && experimentalFeatureSettings.isEnabled(Xp::Flakes))
183183
input = lookupInRegistries(state.store, input).first;
184184

185-
if (state.settings.pureEval && !input.isLocked()) {
185+
if (state.settings.pureEval && !input.isConsideredLocked(state.fetchSettings)) {
186186
auto fetcher = "fetchTree";
187187
if (params.isFetchGit)
188188
fetcher = "fetchGit";

src/libfetchers/fetch-settings.hh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ struct Settings : public Config
7070
Setting<bool> warnDirty{this, true, "warn-dirty",
7171
"Whether to warn about dirty Git/Mercurial trees."};
7272

73+
Setting<bool> allowDirtyLocks{
74+
this,
75+
false,
76+
"allow-dirty-locks",
77+
R"(
78+
Whether to allow dirty inputs (such as dirty Git workdirs)
79+
to be locked via their NAR hash. This is generally bad
80+
practice since Nix has no way to obtain such inputs if they
81+
are subsequently modified. Therefore lock files with dirty
82+
locks should generally only be used for local testing, and
83+
should not be pushed to other users.
84+
)",
85+
{},
86+
true,
87+
Xp::Flakes};
88+
7389
Setting<bool> trustTarballsFromGitForges{
7490
this, true, "trust-tarballs-from-git-forges",
7591
R"(

src/libfetchers/fetchers.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "fetch-to-store.hh"
55
#include "json-utils.hh"
66
#include "store-path-accessor.hh"
7+
#include "fetch-settings.hh"
78

89
#include <nlohmann/json.hpp>
910

@@ -154,6 +155,12 @@ bool Input::isLocked() const
154155
return scheme && scheme->isLocked(*this);
155156
}
156157

158+
bool Input::isConsideredLocked(
159+
const Settings & settings) const
160+
{
161+
return isLocked() || (settings.allowDirtyLocks && getNarHash());
162+
}
163+
157164
bool Input::isFinal() const
158165
{
159166
return maybeGetBoolAttr(attrs, "__final").value_or(false);

src/libfetchers/fetchers.hh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ public:
9595
*/
9696
bool isLocked() const;
9797

98+
/**
99+
* Return whether the input is either locked, or, if
100+
* `allow-dirty-locks` is enabled, it has a NAR hash. In the
101+
* latter case, we can verify the input but we may not be able to
102+
* fetch it from anywhere.
103+
*/
104+
bool isConsideredLocked(
105+
const Settings & settings) const;
106+
98107
/**
99108
* Return whether this is a "final" input, meaning that fetching
100109
* it will not add, remove or change any attributes. (See

src/libflake/flake/flake.cc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup
345345
}
346346

347347
static LockFile readLockFile(
348+
const Settings & settings,
348349
const fetchers::Settings & fetchSettings,
349350
const SourcePath & lockFilePath)
350351
{
@@ -380,6 +381,7 @@ LockedFlake lockFlake(
380381
}
381382

382383
auto oldLockFile = readLockFile(
384+
settings,
383385
state.fetchSettings,
384386
lockFlags.referenceLockFilePath.value_or(
385387
flake.lockFilePath()));
@@ -616,7 +618,7 @@ LockedFlake lockFlake(
616618
inputFlake.inputs, childNode, inputPath,
617619
oldLock
618620
? std::dynamic_pointer_cast<const Node>(oldLock)
619-
: readLockFile(state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(),
621+
: readLockFile(settings, state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(),
620622
oldLock ? lockRootPath : inputPath,
621623
localPath,
622624
false);
@@ -678,9 +680,11 @@ LockedFlake lockFlake(
678680

679681
if (lockFlags.writeLockFile) {
680682
if (sourcePath || lockFlags.outputLockFilePath) {
681-
if (auto unlockedInput = newLockFile.isUnlocked()) {
683+
if (auto unlockedInput = newLockFile.isUnlocked(state.fetchSettings)) {
682684
if (lockFlags.failOnUnlocked)
683-
throw Error("cannot write lock file of flake '%s' because it has an unlocked input ('%s').\n", topRef, *unlockedInput);
685+
throw Error(
686+
"Will not write lock file of flake '%s' because it has an unlocked input ('%s'). "
687+
"Use '--allow-dirty-locks' to allow this anyway.", topRef, *unlockedInput);
684688
if (state.fetchSettings.warnDirty)
685689
warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput);
686690
} else {
@@ -979,9 +983,11 @@ static RegisterPrimOp r4({
979983

980984
}
981985

982-
std::optional<Fingerprint> LockedFlake::getFingerprint(ref<Store> store) const
986+
std::optional<Fingerprint> LockedFlake::getFingerprint(
987+
ref<Store> store,
988+
const fetchers::Settings & fetchSettings) const
983989
{
984-
if (lockFile.isUnlocked()) return std::nullopt;
990+
if (lockFile.isUnlocked(fetchSettings)) return std::nullopt;
985991

986992
auto fingerprint = flake.lockedRef.input.getFingerprint(store);
987993
if (!fingerprint) return std::nullopt;

src/libflake/flake/flake.hh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ struct LockedFlake
129129
*/
130130
std::map<ref<Node>, SourcePath> nodePaths;
131131

132-
std::optional<Fingerprint> getFingerprint(ref<Store> store) const;
132+
std::optional<Fingerprint> getFingerprint(
133+
ref<Store> store,
134+
const fetchers::Settings & fetchSettings) const;
133135
};
134136

135137
struct LockFlags

src/libflake/flake/lockfile.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <nlohmann/json.hpp>
1111

1212
#include "strings.hh"
13+
#include "flake/settings.hh"
1314

1415
namespace nix::flake {
1516

@@ -43,8 +44,8 @@ LockedNode::LockedNode(
4344
, originalRef(getFlakeRef(fetchSettings, json, "original", nullptr))
4445
, isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true)
4546
{
46-
if (!lockedRef.input.isLocked())
47-
throw Error("lock file contains unlocked input '%s'",
47+
if (!lockedRef.input.isConsideredLocked(fetchSettings))
48+
throw Error("Lock file contains unlocked input '%s'. Use '--allow-dirty-locks' to accept this lock file.",
4849
fetchers::attrsToJSON(lockedRef.input.toAttrs()));
4950

5051
// For backward compatibility, lock file entries are implicitly final.
@@ -228,7 +229,7 @@ std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile)
228229
return stream;
229230
}
230231

231-
std::optional<FlakeRef> LockFile::isUnlocked() const
232+
std::optional<FlakeRef> LockFile::isUnlocked(const fetchers::Settings & fetchSettings) const
232233
{
233234
std::set<ref<const Node>> nodes;
234235

@@ -247,7 +248,7 @@ std::optional<FlakeRef> LockFile::isUnlocked() const
247248
for (auto & i : nodes) {
248249
if (i == ref<const Node>(root)) continue;
249250
auto node = i.dynamic_pointer_cast<const LockedNode>();
250-
if (node && (!node->lockedRef.input.isLocked() || !node->lockedRef.input.isFinal()))
251+
if (node && (!node->lockedRef.input.isConsideredLocked(fetchSettings) || !node->lockedRef.input.isFinal()))
251252
return node->lockedRef;
252253
}
253254

src/libflake/flake/lockfile.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct LockFile
7171
* Check whether this lock file has any unlocked or non-final
7272
* inputs. If so, return one.
7373
*/
74-
std::optional<FlakeRef> isUnlocked() const;
74+
std::optional<FlakeRef> isUnlocked(const fetchers::Settings & fetchSettings) const;
7575

7676
bool operator ==(const LockFile & other) const;
7777

src/libflake/flake/settings.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct Settings : public Config
2929
this,
3030
false,
3131
"accept-flake-config",
32-
"Whether to accept nix configuration from a flake without prompting.",
32+
"Whether to accept Nix configuration settings from a flake without prompting.",
3333
{},
3434
true,
3535
Xp::Flakes};

0 commit comments

Comments
 (0)