|
4 | 4 | #include "nix/util/processes.hh" |
5 | 5 | #include "nix/store/builtins.hh" |
6 | 6 | #include "nix/store/path-references.hh" |
| 7 | +#include "nix/store/build/find-cycles.hh" |
7 | 8 | #include "nix/util/finally.hh" |
| 9 | + |
| 10 | +#include <ranges> |
8 | 11 | #include "nix/util/util.hh" |
9 | 12 | #include "nix/util/archive.hh" |
10 | 13 | #include "nix/util/git.hh" |
@@ -62,6 +65,73 @@ struct NotDeterministic : BuildError |
62 | 65 | } |
63 | 66 | }; |
64 | 67 |
|
| 68 | +/** |
| 69 | + * Information about an output path for cycle analysis. |
| 70 | + */ |
| 71 | +struct OutputPathInfo |
| 72 | +{ |
| 73 | + std::string outputName; ///< Name of the output (e.g., "out", "dev") |
| 74 | + StorePath storePath; ///< The StorePath of this output |
| 75 | + Path actualPath; ///< Actual filesystem path where the output is located |
| 76 | +}; |
| 77 | + |
| 78 | +/** |
| 79 | + * Helper to analyze cycles and throw a detailed error. |
| 80 | + * |
| 81 | + * This function always throws - either the detailed cycle error or re-throws |
| 82 | + * the original exception if no cycles are found. |
| 83 | + * |
| 84 | + * @param drvName The formatted name of the derivation being built |
| 85 | + * @param referenceablePaths Set of paths to search for in the outputs |
| 86 | + * @param getOutputPaths Callback returning output information to scan |
| 87 | + */ |
| 88 | +[[noreturn]] static void analyzeCyclesAndThrow( |
| 89 | + std::string_view drvName, |
| 90 | + const StorePathSet & referenceablePaths, |
| 91 | + std::function<std::vector<OutputPathInfo>()> getOutputPaths) |
| 92 | +{ |
| 93 | + debug("cycle detected, analyzing for detailed error report"); |
| 94 | + |
| 95 | + // Scan all outputs for dependency cycles with exact file paths |
| 96 | + std::vector<std::vector<std::string>> allCycles; |
| 97 | + for (auto & output : getOutputPaths()) { |
| 98 | + debug("scanning output '%s' at path '%s' for cycles", output.outputName, output.actualPath); |
| 99 | + |
| 100 | + auto cycles = findDependencyCycles(output.actualPath, output.storePath, referenceablePaths); |
| 101 | + // Move cycles to avoid copying the potentially large cycle data |
| 102 | + std::ranges::move(cycles, std::back_inserter(allCycles)); |
| 103 | + } |
| 104 | + |
| 105 | + if (allCycles.empty()) { |
| 106 | + debug("no detailed cycles found, re-throwing original error"); |
| 107 | + throw; |
| 108 | + } |
| 109 | + |
| 110 | + debug("found %lu cycles", allCycles.size()); |
| 111 | + |
| 112 | + // Build detailed error message |
| 113 | + std::string cycleDetails = fmt("Detailed cycle analysis found %d cycle path(s):", allCycles.size()); |
| 114 | + |
| 115 | + for (const auto & [idx, cycle] : std::views::enumerate(allCycles)) { |
| 116 | + cycleDetails += fmt("\n\nCycle %d:", idx + 1); |
| 117 | + for (auto & node : cycle) { |
| 118 | + cycleDetails += fmt("\n → %s", node); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + cycleDetails += |
| 123 | + fmt("\n\nThis means there are circular references between output files.\n" |
| 124 | + "The build cannot proceed because the outputs reference each other."); |
| 125 | + |
| 126 | + // Add hint with temp paths for debugging |
| 127 | + if (settings.keepFailed || verbosity >= lvlDebug) { |
| 128 | + cycleDetails += "\n\nNote: Temporary build outputs are preserved for inspection."; |
| 129 | + } |
| 130 | + |
| 131 | + throw BuildError( |
| 132 | + BuildResult::Failure::OutputRejected, "cycle detected in build of '%s': %s", drvName, cycleDetails); |
| 133 | +} |
| 134 | + |
65 | 135 | /** |
66 | 136 | * This class represents the state for building locally. |
67 | 137 | * |
@@ -1473,43 +1543,62 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() |
1473 | 1543 | outputStats.insert_or_assign(outputName, std::move(st)); |
1474 | 1544 | } |
1475 | 1545 |
|
1476 | | - auto sortedOutputNames = topoSort( |
1477 | | - outputsToSort, |
1478 | | - {[&](const std::string & name) { |
1479 | | - auto orifu = get(outputReferencesIfUnregistered, name); |
1480 | | - if (!orifu) |
1481 | | - throw BuildError( |
1482 | | - BuildResult::Failure::OutputRejected, |
1483 | | - "no output reference for '%s' in build of '%s'", |
1484 | | - name, |
1485 | | - store.printStorePath(drvPath)); |
1486 | | - return std::visit( |
1487 | | - overloaded{ |
1488 | | - /* Since we'll use the already installed versions of these, we |
1489 | | - can treat them as leaves and ignore any references they |
1490 | | - have. */ |
1491 | | - [&](const AlreadyRegistered &) { return StringSet{}; }, |
1492 | | - [&](const PerhapsNeedToRegister & refs) { |
1493 | | - StringSet referencedOutputs; |
1494 | | - /* FIXME build inverted map up front so no quadratic waste here */ |
1495 | | - for (auto & r : refs.refs) |
1496 | | - for (auto & [o, p] : scratchOutputs) |
1497 | | - if (r == p) |
1498 | | - referencedOutputs.insert(o); |
1499 | | - return referencedOutputs; |
1500 | | - }, |
1501 | | - }, |
1502 | | - *orifu); |
1503 | | - }}, |
1504 | | - {[&](const std::string & path, const std::string & parent) { |
1505 | | - // TODO with more -vvvv also show the temporary paths for manual inspection. |
1506 | | - return BuildError( |
1507 | | - BuildResult::Failure::OutputRejected, |
1508 | | - "cycle detected in build of '%s' in the references of output '%s' from output '%s'", |
1509 | | - store.printStorePath(drvPath), |
1510 | | - path, |
1511 | | - parent); |
1512 | | - }}); |
| 1546 | + auto sortedOutputNames = [&]() { |
| 1547 | + try { |
| 1548 | + return topoSort( |
| 1549 | + outputsToSort, |
| 1550 | + {[&](const std::string & name) { |
| 1551 | + auto orifu = get(outputReferencesIfUnregistered, name); |
| 1552 | + if (!orifu) |
| 1553 | + throw BuildError( |
| 1554 | + BuildResult::Failure::OutputRejected, |
| 1555 | + "no output reference for '%s' in build of '%s'", |
| 1556 | + name, |
| 1557 | + store.printStorePath(drvPath)); |
| 1558 | + return std::visit( |
| 1559 | + overloaded{ |
| 1560 | + /* Since we'll use the already installed versions of these, we |
| 1561 | + can treat them as leaves and ignore any references they |
| 1562 | + have. */ |
| 1563 | + [&](const AlreadyRegistered &) { return StringSet{}; }, |
| 1564 | + [&](const PerhapsNeedToRegister & refs) { |
| 1565 | + StringSet referencedOutputs; |
| 1566 | + /* FIXME build inverted map up front so no quadratic waste here */ |
| 1567 | + for (auto & r : refs.refs) |
| 1568 | + for (auto & [o, p] : scratchOutputs) |
| 1569 | + if (r == p) |
| 1570 | + referencedOutputs.insert(o); |
| 1571 | + return referencedOutputs; |
| 1572 | + }, |
| 1573 | + }, |
| 1574 | + *orifu); |
| 1575 | + }}, |
| 1576 | + {[&](const std::string & path, const std::string & parent) { |
| 1577 | + // TODO with more -vvvv also show the temporary paths for manual inspection. |
| 1578 | + return BuildError( |
| 1579 | + BuildResult::Failure::OutputRejected, |
| 1580 | + "cycle detected in build of '%s' in the references of output '%s' from output '%s'", |
| 1581 | + store.printStorePath(drvPath), |
| 1582 | + path, |
| 1583 | + parent); |
| 1584 | + }}); |
| 1585 | + } catch (std::exception & e) { |
| 1586 | + analyzeCyclesAndThrow(store.printStorePath(drvPath), referenceablePaths, [&]() { |
| 1587 | + std::vector<OutputPathInfo> outputPaths; |
| 1588 | + for (auto & [outputName, _] : drv.outputs) { |
| 1589 | + auto scratchOutput = get(scratchOutputs, outputName); |
| 1590 | + if (scratchOutput) { |
| 1591 | + outputPaths.push_back( |
| 1592 | + OutputPathInfo{ |
| 1593 | + .outputName = outputName, |
| 1594 | + .storePath = *scratchOutput, |
| 1595 | + .actualPath = realPathInSandbox(store.printStorePath(*scratchOutput))}); |
| 1596 | + } |
| 1597 | + } |
| 1598 | + return outputPaths; |
| 1599 | + }); |
| 1600 | + } |
| 1601 | + }(); |
1513 | 1602 |
|
1514 | 1603 | std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); |
1515 | 1604 |
|
@@ -1848,12 +1937,24 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() |
1848 | 1937 | /* Register each output path as valid, and register the sets of |
1849 | 1938 | paths referenced by each of them. If there are cycles in the |
1850 | 1939 | outputs, this will fail. */ |
1851 | | - { |
| 1940 | + try { |
1852 | 1941 | ValidPathInfos infos2; |
1853 | 1942 | for (auto & [outputName, newInfo] : infos) { |
1854 | 1943 | infos2.insert_or_assign(newInfo.path, newInfo); |
1855 | 1944 | } |
1856 | 1945 | store.registerValidPaths(infos2); |
| 1946 | + } catch (BuildError & e) { |
| 1947 | + analyzeCyclesAndThrow(store.printStorePath(drvPath), referenceablePaths, [&]() { |
| 1948 | + std::vector<OutputPathInfo> outputPaths; |
| 1949 | + for (auto & [outputName, newInfo] : infos) { |
| 1950 | + outputPaths.push_back( |
| 1951 | + OutputPathInfo{ |
| 1952 | + .outputName = outputName, |
| 1953 | + .storePath = newInfo.path, |
| 1954 | + .actualPath = store.toRealPath(store.printStorePath(newInfo.path))}); |
| 1955 | + } |
| 1956 | + return outputPaths; |
| 1957 | + }); |
1857 | 1958 | } |
1858 | 1959 |
|
1859 | 1960 | /* If we made it this far, we are sure the output matches the |
|
0 commit comments