Skip to content

feat(libstore): make cycle errors more verbose#6

Open
milahu wants to merge 61 commits into
masterfrom
better-cycle-errors-2026-05-14
Open

feat(libstore): make cycle errors more verbose#6
milahu wants to merge 61 commits into
masterfrom
better-cycle-errors-2026-05-14

Conversation

@milahu

@milahu milahu commented May 17, 2026

Copy link
Copy Markdown
Owner

continue NixOS#14218

example output with c1ea0d4

$ pwd
/home/user/src/nixos/nix/build

$ sudo NIX_REMOTE=local ./src/nix/nix-build ../tests/functional/multiple-outputs.nix -A cyclic 
this derivation will be built:
  /nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv
building '/nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv'...
error: cycle detected in build of '/nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv' in the references of output 'bin' from output 'dev'

       Found 20 cycle edges:

       1:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths/out-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/fullpaths/dev-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/fullpaths/bin-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths/out-to-dev

       2:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/fullpaths-dirpaths/nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs

       3:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/fullpaths-filepaths/nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs

       4:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths2/out-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/fullpaths2/bin-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/fullpaths2/dev-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths2/out-to-bin

       5:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/hashes/bin-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk

       6:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/relpaths/bin-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs

       7:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/relsymlinks/bin-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs

       8:
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin/symlinks/bin-to-out
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs

       9:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/fullpaths-dirpaths/nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin

       10:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/fullpaths-filepaths/nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin

       11:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/hashes/dev-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k

       12:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/relpaths/dev-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin

       13:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/relsymlinks/dev-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin

       14:
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev/symlinks/dev-to-bin
         - /nix/store/cc12jzz09fb65qldmfwzw07m2z1cn52k-cyclic-outputs-bin

       15:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths-dirpaths/nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev

       16:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/fullpaths-filepaths/nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev

       17:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/hashes/out-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz

       18:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/relpaths/out-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev

       19:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/relsymlinks/out-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev

       20:
         - /nix/store/q8p9zp067f7p7rhbhbfx2ap297zg7npk-cyclic-outputs/symlinks/out-to-dev
         - /nix/store/w72svchin1z6x572b4227y6mw3ajlsyz-cyclic-outputs-dev

       error: cycle detected in build of '/nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv' in the references of output 'bin' from output 'dev'

note: to make this work i had to patch multiple-outputs.nix

--- a/tests/functional/multiple-outputs.nix
+++ b/tests/functional/multiple-outputs.nix
@@ -1,4 +1,5 @@
-with import ./config.nix;
+{ pkgs ? import <nixpkgs> { } }:
+with pkgs.stdenv;
 
 rec {
 

if getDetailedCycleError fails to find a cycle
then we also show an internal error

$ sudo NIX_REMOTE=local ./src/nix/nix-build ../tests/functional/multiple-outputs.nix -A cyclic 
this derivation will be built:
  /nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv
building '/nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv'...
error: cycle detected in build of '/nix/store/8ay8v3y37n9mnagxhxrjccawcapghn13-cyclic-outputs.drv' in the references of output 'bin' from output 'dev'

       internal error: getDetailedCycleError found no cycle edges. this is a bug in nix, please report it.
real world example: libtorrent

edges 1 + 2 = cycle: out → dev → out
edge 3 may be a false positive

$ sudo NIX_REMOTE=local ./src/nix/nix-build ~/src/milahu/nur-packages -A libtorrent-rasterbar-2_1_x --keep-failed

error: cycle detected in build of '/nix/store/2c3ar0bf5bhj33dx2za8r1vnmy40j891-libtorrent-rasterbar-2.1.0-pre-2026-05-14.drv' in the references of output 'dev' from output 'out'

       Found 3 cycle edges:

       1:
         - /nix/store/phzychr4a64692l2wh3b7113vc1m463l-libtorrent-rasterbar-2.1.0-pre-2026-05-14-dev/lib/cmake/LibtorrentRasterbar/LibtorrentRasterbarTargets-release.cmake
         - /nix/store/bd0m95adrnycfabs3i76fnb3m6bj51ak-libtorrent-rasterbar-2.1.0-pre-2026-05-14/lib/libtorrent-rasterbar.so.2.1.0

       2:
         - /nix/store/bd0m95adrnycfabs3i76fnb3m6bj51ak-libtorrent-rasterbar-2.1.0-pre-2026-05-14/nix/store/bd0m95adrnycfabs3i76fnb3m6bj51ak-libtorrent-rasterbar-2.1.0-pre-2026-05-14/lib/pkgconfig/libtorrent-rasterbar.pc
         - /nix/store/phzychr4a64692l2wh3b7113vc1m463l-libtorrent-rasterbar-2.1.0-pre-2026-05-14-dev/include

       3:
         - /nix/store/y6rd8lni1mz5j2z5si7klb15x1w8qmm7-libtorrent-rasterbar-2.1.0-pre-2026-05-14-python/lib/python3.13/site-packages/libtorrent.cpython-313-x86_64-linux-gnu.so
         - /nix/store/bd0m95adrnycfabs3i76fnb3m6bj51ak-libtorrent-rasterbar-2.1.0-pre-2026-05-14/lib

       Note: Build outputs are kept for inspection.
       You can examine the files listed above to understand the cycle.

       error: cycle detected in build of '/nix/store/2c3ar0bf5bhj33dx2za8r1vnmy40j891-libtorrent-rasterbar-2.1.0-pre-2026-05-14.drv' in the references of output 'dev' from output 'out'

ping @lovesegfault @ConnorBaker @xokdvium @Ericson2314 @edolstra @Dessix @nrdxp @SuperSandro2000 @jonringer @copumpkin @vcunat @datakurre @jtojnar @Artturin @arianvp @charles-dyfis-net @peterwaller-arm @abbradar @gahara @jb55

lovesegfault and others added 30 commits May 15, 2026 08:18
Co-Authored-By: Milan Hauth <milahu@gmail.com>
Previously, `CycleEdgeScanSink::operator()` copied the entire
`getResult()` `StringSet` twice on every 64KB chunk to detect newly
found hashes. For large files, this created O(n * chunks) overhead.

Now we track which hashes have been recorded for the current file using
`recordedForCurrentFile`, avoiding the set copies. The insert() returns
true only for newly seen hashes, making this O(1) per hash found.
The `hashPathMap` was being passed to `CycleEdgeScanSink` and stored as
a member variable, but was never actually used. The sink only needs the
hash strings for detection via `RefScanSink`, not the full `StorePath`
mapping.
Moved the `edges` member variable from public to private section for
better encapsulation. Access is provided through the `getEdges()`
method.
Replaced raw `read()` with `readFull()` helper, which properly
handles partial reads and `EINTR`. The previous code manually checked
for errors but didn't handle the case where `read()` returns fewer
bytes than requested.
After refactoring to use `RefScanSink`, we no longer manually search for
hashes in buffers, so the `refLength` constant (hash length) is unused.
`RefScanSink` handles this internally.
The single-pass greedy algorithm could fail to connect all edges if
they arrived in certain orders. For example, edges A→B, C→D, D→A, B→C
would result in two paths [D→A→B→C] and [C→D] instead of one complete
cycle.

Added a second pass that repeatedly tries to merge existing multiedges
with each other until no more merges are possible. This ensures we find
complete cycle paths regardless of edge discovery order.
…File APIs

Replaced POSIX-specific file operations with portable alternatives to
improve Windows compatibility.
Added explicit `#include "nix/util/archive.hh"` since we use the
caseHackSuffix constant from it.
Replace string concatenation for path joining with type-safe
`std::filesystem::path operator/`.
…anPath

The "2" suffix was unclear and didn't communicate the function's purpose.
The new name better describes what it does: walks the filesystem tree and
scans each file using the provided sink.
Changed `walkAndScanPath` to take `std::filesystem::path` instead of `string`,
eliminating repeated string→path conversions throughout the function.
…acOS only

Since caseHackSuffix is only used inside `#ifdef __APPLE__` blocks, gate
the archive.hh include with the same condition.
Move `find-cycles.hh` to `src/libstore/include/nix/store/build/` to
ensure it is properly installed as a public header and can be used by
tests and other consumers of the library.
Replace the multi-pass O(n²) algorithm with a single-pass O(n) approach
using hashmaps to track path endpoints.

Before:
- First pass: O(n*m) to join each edge with existing multiedges
- Second pass: O(m²) repeated merging until no more merges possible
- Required multiple iterations through the entire dataset

Now:
- Single O(n) pass through edges
- Two hashmaps (pathStartingAt, pathEndingAt) enable O(1) lookups
- Immediately identifies connections without linear searches
- Handles all cases in one pass:
  * Extending existing paths (prepend/append)
  * Joining two separate paths
  * Forming cycles when a path connects to itself
Replace direct `std::filesystem` operations with `SourceAccessor`..
milahu added 29 commits May 17, 2026 12:37
revert parts of
feat(libstore/find-cycles): try to find file paths

revert back to type
using StoreCycleEdge = std::deque<std::string>;

restore the function transformEdgesToMultiedges
before this change, also buildInputs were found as cycle edges

but actual cycles cannot include buildInputs
because buildInputs dont know this derivation's output paths

fix failing test cyclic-fullpaths-buildInputs
fix failing test cyclic-fullpaths-self
fix failing test cyclic-fullpaths-duplicate
so the file path with the shortest derivation path comes first
@milahu milahu force-pushed the better-cycle-errors-2026-05-14 branch from 2b370ff to e5e567d Compare May 23, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants