GTTF: Print much better error message for dependency cycles.
Example:
gyp: Cycles in .gyp file dependency graph detected:
Cycle: build/linux/system.gyp -> third_party/zlib/zlib.gyp -> base/base.gyp -> tools/xdisplaycheck/xdisplaycheck.gyp -> build/linux/system.gyp
Cycle: build/linux/system.gyp -> third_party/zlib/zlib.gyp -> base/base.gyp -> build/linux/system.gyp
Cycle: base/base.gyp -> tools/xdisplaycheck/xdisplaycheck.gyp -> build/linux/system.gyp -> base/base.gyp
Cycle: build/linux/system.gyp -> base/base.gyp -> tools/xdisplaycheck/xdisplaycheck.gyp -> build/linux/system.gyp
Cycle: base/base.gyp -> build/linux/system.gyp -> base/base.gyp
Cycle: base/base.gyp -> tools/xdisplaycheck/xdisplaycheck.gyp -> build/linux/system.gyp -> third_party/zlib/zlib.gyp -> base/base.gyp
Cycle: base/base.gyp -> build/linux/system.gyp -> third_party/zlib/zlib.gyp -> base/base.gyp
Cycle: build/linux/system.gyp -> base/base.gyp -> build/linux/system.gyp
BUG=chromium:35878
Patch by Paweł Hajdan Jr. <phajdan.jr@chromium.org>
Take 2, originally landed in r1695 and backed out in r1696.
Review URL: https://codereview.chromium.org/23018008/
git-svn-id: http://gyp.googlecode.com/svn/trunk@1700 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/input.py b/pylib/gyp/input.py
index 0d9beb0..0c303d2 100644
--- a/pylib/gyp/input.py
+++ b/pylib/gyp/input.py
@@ -1443,6 +1443,9 @@
self.dependencies = []
self.dependents = []
+ def __repr__(self):
+ return '<DependencyGraphNode: %r>' % self.ref
+
def FlattenToList(self):
# flat_list is the sorted list of dependencies - actually, the list items
# are the "ref" attributes of DependencyGraphNodes. Every target will
@@ -1485,6 +1488,27 @@
return flat_list
+ def FindCycles(self, path=None):
+ """
+ Returns a list of cycles in the graph, where each cycle is its own list.
+ """
+ if path is None:
+ path = [self]
+
+ results = []
+ for node in self.dependents:
+ if node in path:
+ cycle = [node]
+ for part in path:
+ cycle.append(part)
+ if part == node:
+ break
+ results.append(tuple(cycle))
+ else:
+ results.extend(node.FindCycles([node] + path))
+
+ return list(set(results))
+
def DirectDependencies(self, dependencies=None):
"""Returns a list of just direct dependencies."""
if dependencies == None:
@@ -1717,10 +1741,16 @@
for file in dependency_nodes.iterkeys():
if not file in flat_list:
bad_files.append(file)
+ common_path_prefix = os.path.commonprefix(dependency_nodes)
+ cycles = []
+ for cycle in root_node.FindCycles():
+ simplified_paths = []
+ for node in cycle:
+ assert(node.ref.startswith(common_path_prefix))
+ simplified_paths.append(node.ref[len(common_path_prefix):])
+ cycles.append('Cycle: %s' % ' -> '.join(simplified_paths))
raise DependencyGraphNode.CircularException, \
- 'Some files not reachable, cycle in .gyp file dependency graph ' + \
- 'detected involving some or all of: ' + \
- ' '.join(bad_files)
+ 'Cycles in .gyp file dependency graph detected:\n' + '\n'.join(cycles)
def DoDependentSettings(key, flat_list, targets, dependency_nodes):
diff --git a/pylib/gyp/input_test.py b/pylib/gyp/input_test.py
new file mode 100755
index 0000000..cdbf6b2
--- /dev/null
+++ b/pylib/gyp/input_test.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+# Copyright 2013 Google Inc. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for the input.py file."""
+
+import gyp.input
+import unittest
+import sys
+
+
+class TestFindCycles(unittest.TestCase):
+ def setUp(self):
+ self.nodes = {}
+ for x in ('a', 'b', 'c', 'd', 'e'):
+ self.nodes[x] = gyp.input.DependencyGraphNode(x)
+
+ def _create_dependency(self, dependent, dependency):
+ dependent.dependencies.append(dependency)
+ dependency.dependents.append(dependent)
+
+ def test_no_cycle_empty_graph(self):
+ for label, node in self.nodes.iteritems():
+ self.assertEquals([], node.FindCycles())
+
+ def test_no_cycle_line(self):
+ self._create_dependency(self.nodes['a'], self.nodes['b'])
+ self._create_dependency(self.nodes['b'], self.nodes['c'])
+ self._create_dependency(self.nodes['c'], self.nodes['d'])
+
+ for label, node in self.nodes.iteritems():
+ self.assertEquals([], node.FindCycles())
+
+ def test_no_cycle_dag(self):
+ self._create_dependency(self.nodes['a'], self.nodes['b'])
+ self._create_dependency(self.nodes['a'], self.nodes['c'])
+ self._create_dependency(self.nodes['b'], self.nodes['c'])
+
+ for label, node in self.nodes.iteritems():
+ self.assertEquals([], node.FindCycles())
+
+ def test_cycle_self_reference(self):
+ self._create_dependency(self.nodes['a'], self.nodes['a'])
+
+ self.assertEquals([(self.nodes['a'], self.nodes['a'])],
+ self.nodes['a'].FindCycles())
+
+ def test_cycle_two_nodes(self):
+ self._create_dependency(self.nodes['a'], self.nodes['b'])
+ self._create_dependency(self.nodes['b'], self.nodes['a'])
+
+ self.assertEquals([(self.nodes['a'], self.nodes['b'], self.nodes['a'])],
+ self.nodes['a'].FindCycles())
+ self.assertEquals([(self.nodes['b'], self.nodes['a'], self.nodes['b'])],
+ self.nodes['b'].FindCycles())
+
+ def test_two_cycles(self):
+ self._create_dependency(self.nodes['a'], self.nodes['b'])
+ self._create_dependency(self.nodes['b'], self.nodes['a'])
+
+ self._create_dependency(self.nodes['b'], self.nodes['c'])
+ self._create_dependency(self.nodes['c'], self.nodes['b'])
+
+ cycles = self.nodes['a'].FindCycles()
+ self.assertTrue(
+ (self.nodes['a'], self.nodes['b'], self.nodes['a']) in cycles)
+ self.assertTrue(
+ (self.nodes['b'], self.nodes['c'], self.nodes['b']) in cycles)
+ self.assertEquals(2, len(cycles))
+
+ def test_big_cycle(self):
+ self._create_dependency(self.nodes['a'], self.nodes['b'])
+ self._create_dependency(self.nodes['b'], self.nodes['c'])
+ self._create_dependency(self.nodes['c'], self.nodes['d'])
+ self._create_dependency(self.nodes['d'], self.nodes['e'])
+ self._create_dependency(self.nodes['e'], self.nodes['a'])
+
+ self.assertEquals([(self.nodes['a'],
+ self.nodes['b'],
+ self.nodes['c'],
+ self.nodes['d'],
+ self.nodes['e'],
+ self.nodes['a'])],
+ self.nodes['a'].FindCycles())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/small/gyptest-small.py b/test/small/gyptest-small.py
index 0e33f28..a8d61fb 100755
--- a/test/small/gyptest-small.py
+++ b/test/small/gyptest-small.py
@@ -30,6 +30,7 @@
'pylib/gyp/generator/ninja_test.py',
'pylib/gyp/generator/xcode_test.py',
'pylib/gyp/common_test.py',
+ 'pylib/gyp/input_test.py',
]
# Collect all the suites from the above files.