nodejs/node · error · DependencyGraphNode.CircularException
Cycles in .gyp file dependency graph detected: {cycles}
Error message
Cycles in .gyp file dependency graph detected:
{cycles} What it means
The file-level analog of error 504. After building the .gyp-file dependency graph and flattening from root_node, if the flattened count != number of file nodes, a cycle exists among .gyp files. gyp attaches the first file to root_node if needed, enumerates cycles via FindCycles, and raises CircularException listing file-level cycles (fileA -> fileB -> fileA).
Source
Thrown at tools/gyp/pylib/gyp/input.py:2036
build_file_node.dependencies.append(root_node)
root_node.dependents.append(build_file_node)
flat_list = root_node.FlattenToList()
# If there's anything left unvisited, there must be a circular dependency
# (cycle).
if len(flat_list) != len(dependency_nodes):
if not root_node.dependents:
# If all files have dependencies, add the first file as a dependent
# of root_node so that the cycle can be discovered from root_node.
file_node = next(iter(dependency_nodes.values()))
file_node.dependencies.append(root_node)
root_node.dependents.append(file_node)
cycles = []
for cycle in root_node.FindCycles():
paths = [node.ref for node in cycle]
cycles.append("Cycle: %s" % " -> ".join(paths))
raise DependencyGraphNode.CircularException(
"Cycles in .gyp file dependency graph detected:\n" + "\n".join(cycles)
)
def DoDependentSettings(key, flat_list, targets, dependency_nodes):
# key should be one of all_dependent_settings, direct_dependent_settings,
# or link_settings.
for target in flat_list:
target_dict = targets[target]
build_file = gyp.common.BuildFile(target)
if key == "all_dependent_settings":
dependencies = dependency_nodes[target].DeepDependencies()
elif key == "direct_dependent_settings":
dependencies = dependency_nodes[target].DirectAndImportedDependencies(
targets
)View on GitHub (pinned to 1b2de5e052)
Solutions
- Break the file-level ring by removing one cross-file dependency edge.
- Move the contested target(s) into a single file or a third shared file so the dependency becomes one-directional.
- Use settings propagation (all_dependent_settings) instead of a hard dependency if only settings are needed.
Example fix
// before: a.gyp depends on b.gyp and b.gyp depends on a.gyp // move shared target into common.gyp; both depend on common.gyp only
Defensive patterns
Strategy: validation
Validate before calling
# file-level cycle check
def file_cycle(file_deps):
color = {}
def dfs(n):
color[n] = 1
for m in file_deps.get(n, []):
if color.get(m) == 1: return True
if m not in color and dfs(m): return True
color[n] = 2
return False
return any(n not in color and dfs(n) for n in file_deps)
Try / catch
try:
gyp.main(args)
except gyp.input.DependencyGraphNode.CircularException as e:
if '.gyp file' in str(e): print('File-level include cycle'); raise Prevention
- Keep .gyp file includes acyclic (one direction).
- Centralize shared targets in a leaf .gyp.
- Review cross-file dependencies during refactor.
When it happens
Trigger: Two or more .gyp files include/depend on each other in a ring at the file level (A.gyp depends on B.gyp which depends on A.gyp), detected by the length mismatch in VerifyNoGYPFileCircularDependencies.
Common situations: Splitting a monolithic .gyp into two files that both reference each other's targets; refactor that introduced mutual includes; cyclic includes via the dependencies arrays across files.
Related errors
- Cycles in dependency graph detected: {cycles}
- CycleError: cycle involving: %s
- Found wildcard in {dependency_key} of {target} referring to
- Dependency '%s' not found while trying to load target %s
- Dependency '%s' not found
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/e1d75d680edee551.
Report an issue: GitHub.