Allow specifying alternative paths for reading/writing flake locks

This allows having multiple separate lockfiles for a single
project, which can be useful for testing against different versions of
nixpkgs; it also allows tracking custom input overrides for remote
flakes without requiring local clones of these flakes.

For example, if I want to build Nix against my locally pinned nixpkgs,
and have a lock file tracking this override independently of future
updates to said nixpkgs:

nix flake lock --output-lock-file /tmp/nix-flake.lock --override-input nixpkgs flake:nixpkgs
nix build --reference-lock-file /tmp/nix-flake.lock

Co-Authored-By: Will Fancher <elvishjerricco@gmail.com>
This commit is contained in:
Linus Heckemann 2023-03-13 21:08:52 +01:00
parent 4a96125c3c
commit e91596eb69
3 changed files with 47 additions and 12 deletions

View file

@ -102,6 +102,28 @@ MixFlakeOptions::MixFlakeOptions()
}} }}
}); });
addFlag({
.longName = "reference-lock-file",
.description = "Read the given lock file instead of `flake.lock` within the top-level flake",
.category = category,
.labels = {"flake-lock-path"},
.handler = {[&](std::string lockFilePath) {
lockFlags.referenceLockFilePath = lockFilePath;
}},
.completer = completePath
});
addFlag({
.longName = "output-lock-file",
.description = "Write the given lock file instead of `flake.lock` within the top-level flake",
.category = category,
.labels = {"flake-lock-path"},
.handler = {[&](std::string lockFilePath) {
lockFlags.outputLockFilePath = lockFilePath;
}},
.completer = completePath
});
addFlag({ addFlag({
.longName = "inputs-from", .longName = "inputs-from",
.description = "Use the inputs of the specified flake as registry entries.", .description = "Use the inputs of the specified flake as registry entries.",

View file

@ -337,7 +337,8 @@ LockedFlake lockFlake(
// FIXME: symlink attack // FIXME: symlink attack
auto oldLockFile = LockFile::read( auto oldLockFile = LockFile::read(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock"); lockFlags.referenceLockFilePath.value_or(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock"));
debug("old lock file: %s", oldLockFile); debug("old lock file: %s", oldLockFile);
@ -619,13 +620,20 @@ LockedFlake lockFlake(
debug("new lock file: %s", newLockFile); debug("new lock file: %s", newLockFile);
auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock";
auto sourcePath = topRef.input.getSourcePath();
auto outputLockFilePath = sourcePath ? std::optional{*sourcePath + "/" + relPath} : std::nullopt;
if (lockFlags.outputLockFilePath) {
outputLockFilePath = lockFlags.outputLockFilePath;
}
/* Check whether we need to / can write the new lock file. */ /* Check whether we need to / can write the new lock file. */
if (!(newLockFile == oldLockFile)) { if (newLockFile != oldLockFile || lockFlags.outputLockFilePath) {
auto diff = LockFile::diff(oldLockFile, newLockFile); auto diff = LockFile::diff(oldLockFile, newLockFile);
if (lockFlags.writeLockFile) { if (lockFlags.writeLockFile) {
if (auto sourcePath = topRef.input.getSourcePath()) { if (outputLockFilePath) {
if (auto unlockedInput = newLockFile.isUnlocked()) { if (auto unlockedInput = newLockFile.isUnlocked()) {
if (fetchSettings.warnDirty) if (fetchSettings.warnDirty)
warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput); warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput);
@ -633,25 +641,24 @@ LockedFlake lockFlake(
if (!lockFlags.updateLockFile) if (!lockFlags.updateLockFile)
throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef); throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef);
auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock"; bool lockFileExists = pathExists(*outputLockFilePath);
auto path = *sourcePath + "/" + relPath;
bool lockFileExists = pathExists(path);
if (lockFileExists) { if (lockFileExists) {
auto s = chomp(diff); auto s = chomp(diff);
if (s.empty()) if (s.empty())
warn("updating lock file '%s'", path); warn("updating lock file '%s'", *outputLockFilePath);
else else
warn("updating lock file '%s':\n%s", path, s); warn("updating lock file '%s':\n%s", *outputLockFilePath, s);
} else } else
warn("creating lock file '%s'", path); warn("creating lock file '%s'", *outputLockFilePath);
newLockFile.write(path); newLockFile.write(*outputLockFilePath);
std::optional<std::string> commitMessage = std::nullopt; std::optional<std::string> commitMessage = std::nullopt;
if (lockFlags.commitLockFile) { if (lockFlags.commitLockFile) {
if (lockFlags.outputLockFilePath) {
throw Error("--commit-lock-file and --output-lock-file are currently incompatible");
}
std::string cm; std::string cm;
cm = fetchSettings.commitLockFileSummary.get(); cm = fetchSettings.commitLockFileSummary.get();

View file

@ -117,6 +117,12 @@ struct LockFlags
/* Whether to commit changes to flake.lock. */ /* Whether to commit changes to flake.lock. */
bool commitLockFile = false; bool commitLockFile = false;
/* The path to a lock file to read instead of the `flake.lock` file in the top-level flake */
std::optional<std::string> referenceLockFilePath = std::nullopt;
/* The path to a lock file to write to instead of the `flake.lock` file in the top-level flake */
std::optional<Path> outputLockFilePath = std::nullopt;
/* Flake inputs to be overridden. */ /* Flake inputs to be overridden. */
std::map<InputPath, FlakeRef> inputOverrides; std::map<InputPath, FlakeRef> inputOverrides;