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

  1. Break the file-level ring by removing one cross-file dependency edge.
  2. Move the contested target(s) into a single file or a third shared file so the dependency becomes one-directional.
  3. 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

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


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/e1d75d680edee551. Report an issue: GitHub.