libstore/local-derivation-goal: show tree with references that lead to an output cycle

Closes #551

This adds a special accessor that falls back to checking if a store-path
exists within a chroot if it's not a valid path. That way,
`genGraphString` can find out which files have which references before
the outputs are registered.

Change-Id: I03c9d508fa3c72e5c262194461a25d71f3f4de15
This commit is contained in:
Maximilian Bosch
2025-08-24 01:15:15 +02:00
parent 4886e506f2
commit e2641cb890
6 changed files with 132 additions and 15 deletions
+25
View File
@@ -0,0 +1,25 @@
---
synopsis: "show tree with references that lead to an output cycle"
issues: [fj#551]
category: Improvements
credits: [ma27]
---
When Lix determines a cyclic dependency between several outputs of a derivation,
it now displays which files in which outputs lead to an output cycle:
```
error: cycle detected in build of '/nix/store/gc5h2whz3rylpf34n99nswvqgkjkigmy-demo.drv' in the references of output 'bar' from output 'foo'.
Shown below are the files inside the outputs leading to the cycle:
/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
└───lib/libfoo: …stuffbefore /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz andafter.…
→ /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz
└───share/snenskek: …???? /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo ....…
→ /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo
└───bin/alarm: …textexttext/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar abcabcabc.…
→ /nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
```
Please note that showing the files and its contents while displaying the cycles only works
on Linux.
+37 -14
View File
@@ -1829,19 +1829,44 @@ try {
return dependencies;
}});
auto sortedOutputNames = std::visit(overloaded {
[&](Cycle<std::string> & cycle) -> std::vector<std::string> {
// TODO with more -vvvv also show the temporary paths for manual inspection.
throw BuildError(
"cycle detected in build of '%s' in the references of output '%s' from output "
"'%s'",
worker.store.printStorePath(drvPath),
cycle.path,
cycle.parent
);
auto & localStore = getLocalStore();
auto sortedOutputNames = TRY_AWAIT(std::visit(
overloaded{
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[&](Cycle<std::string> & cycle) -> kj::Promise<Result<std::vector<std::string>>> {
try {
auto chrootAwareAccessor = getChrootDirAwareFSAccessor();
auto graphStr = TRY_AWAIT(genGraphString(
scratchOutputs.at(cycle.path),
scratchOutputs.at(cycle.parent),
outputGraph,
worker.store,
true,
// We need to access store-paths that aren't registered yet for
// precise=true. Hence, only do this if a chroot-aware accessor is
// implemented in for this platform.
chrootAwareAccessor.has_value(),
chrootAwareAccessor
));
throw BuildError(
"cycle detected in build of '%s' in the references of output '%s' from "
"output "
"'%s'.\n\nShown below are the files inside the outputs leading to the "
"cycle:\n%s",
worker.store.printStorePath(drvPath),
cycle.path,
cycle.parent,
Uncolored(graphStr)
);
} catch (...) {
co_return result::current_exception();
}
},
[](auto & r) -> kj::Promise<Result<std::vector<std::string>>> { co_return r; }
},
[&](auto & r) { return r; }
}, topoSortedOutputs);
topoSortedOutputs
));
std::reverse(sortedOutputNames.begin(), sortedOutputNames.end());
@@ -2111,8 +2136,6 @@ try {
}
}
auto & localStore = getLocalStore();
// Check determinism and run the diff hook for input-addressed
// paths if we're in check mode.
// TODO: implement this for content-addressed paths too.
@@ -3,6 +3,7 @@
#include "lix/libstore/build/derivation-goal.hh"
#include "lix/libstore/local-store.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/cgroup.hh"
@@ -329,6 +330,15 @@ protected:
*/
virtual void setupSyscallFilter() {}
/**
* Create a special accessor that can access paths that were built within the sandbox's
* chroot.
*/
virtual std::optional<ref<FSAccessor>> getChrootDirAwareFSAccessor()
{
return std::nullopt;
};
/**
* Execute the builder, replacing the current process.
* Generally this means an `execve` call.
+45
View File
@@ -13,6 +13,7 @@
#include <csignal>
#include <cstdlib>
#include <grp.h>
#include <memory>
#include <regex>
#include <sys/prctl.h>
@@ -1176,4 +1177,48 @@ void LinuxLocalDerivationGoal::killSandbox(bool getStats)
}
}
}
struct ChrootDirAwareFSAccessor : public LocalStoreAccessor
{
Path chrootDir;
ChrootDirAwareFSAccessor(ref<LocalFSStore> store, const Path & chrootDir)
: LocalStoreAccessor(store)
, chrootDir(chrootDir)
{
}
kj::Promise<Result<Path>> toRealPath(const Path & path, bool requireValidPath = true) override
try {
auto storePath = store->toStorePath(path).first;
if (!TRY_AWAIT(store->isValidPath(storePath))) {
auto chrootStorePath = chrootDir + "/" + path;
if (pathExists(chrootStorePath)) {
co_return chrootStorePath;
}
if (requireValidPath) {
throw InvalidPath(
"path '%1%' does not exist in the store, neither does chrooted path '%2%'",
store->printStorePath(storePath),
chrootStorePath
);
}
}
co_return TRY_AWAIT(LocalStoreAccessor::toRealPath(path, false));
} catch (...) {
co_return result::current_exception();
}
};
std::optional<ref<FSAccessor>> LinuxLocalDerivationGoal::getChrootDirAwareFSAccessor()
{
return make_ref<ChrootDirAwareFSAccessor>(
ref<LocalFSStore>::unsafeFromPtr(
std::dynamic_pointer_cast<LocalFSStore>(getLocalStore().shared_from_this())
),
chrootRootDir
);
}
}
+6
View File
@@ -58,6 +58,12 @@ private:
RunningProgram pastaPid;
/**
* Create a special accessor that can access paths that were built within the sandbox's
* chroot.
*/
std::optional<ref<FSAccessor>> getChrootDirAwareFSAccessor() override;
/**
* Create and populate chroot
*/
+9 -1
View File
@@ -2,7 +2,15 @@ source common.sh
clearStore
(! nix-build check-outputs.nix -A cycle) 2>&1 | grepQuiet "cycle detected in build of '.*' in the references of output 'bar' from output 'foo'"
error="$(! nix-build check-outputs.nix -A cycle 2>&1)"
grepQuiet "cycle detected in build of '.*' in the references of output 'bar' from output 'foo'" <<<"$error"
if [[ "$(uname -s)" = Linux ]]; then
<<<"$error" grepQuiet "/store/.*-cycle-bar"
<<<"$error" grepQuiet "└───lib/libfoo: ….*cycle-baz.*"
<<<"$error" grepQuiet " →.*/store/.*-cycle-baz"
<<<"$error" grepQuiet " └───share/lalala:.*-cycle-foo.*"
fi
error="$(! nix-build check-outputs.nix -A as_dependency 2>&1)"