diff --git a/gclient.py b/gclient.py index 36a6f119e6..e3ee4e98cc 100755 --- a/gclient.py +++ b/gclient.py @@ -544,6 +544,8 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): self.set_url(url) def ToLines(self): + # () -> Sequence[str] + """Returns strings representing the deps (info, graphviz line)""" s = [] condition_part = ([' "condition": %r,' % self.condition] if self.condition else []) @@ -1322,16 +1324,20 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): def __repr__(self): return '%s: %s' % (self.name, self.url) - def hierarchy(self, include_url=True): + def hierarchy(self, include_url=True, graphviz=False): """Returns a human-readable hierarchical reference to a Dependency.""" def format_name(d): if include_url: return '%s(%s)' % (d.name, d.url) - return d.name + return '"%s"' % d.name # quotes required for graph dot file. + out = format_name(self) i = self.parent while i and i.name: out = '%s -> %s' % (format_name(i), out) + if graphviz: + # for graphviz we just need each parent->child relationship listed once. + return out i = i.parent return out @@ -2275,10 +2281,13 @@ class CipdDependency(Dependency): self.url, self.root.root_dir, self.name, self.outbuf, out_cb, root=self._cipd_root, package=self._cipd_package) - def hierarchy(self, include_url=False): + def hierarchy(self, include_url=False, graphviz=False): + if graphviz: + return '' # graphviz lines not implemented for cipd deps. return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir def ToLines(self): + # () -> Sequence[str] """Return a list of lines representing this in a DEPS file.""" def escape_cipd_var(package): return package.replace('{', '{{').replace('}', '}}') @@ -2393,6 +2402,7 @@ class Flattener(object): self._client = client self._deps_string = None + self._deps_graph_lines = None self._deps_files = set() self._allowed_hosts = set() @@ -2408,6 +2418,11 @@ class Flattener(object): assert self._deps_string is not None return self._deps_string + @property + def deps_graph_lines(self): + assert self._deps_graph_lines is not None + return self._deps_graph_lines + @property def deps_files(self): return self._deps_files @@ -2464,16 +2479,17 @@ class Flattener(object): gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from, self._client.dependencies[0]) + + self._deps_graph_lines = _DepsToDotGraphLines(self._deps) self._deps_string = '\n'.join( _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args) + - _AllowedHostsToLines(self._allowed_hosts) + - _DepsToLines(self._deps) + + _AllowedHostsToLines(self._allowed_hosts) + _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) + _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) + - _VarsToLines(self._vars) + - ['# %s, %s' % (url, deps_file) - for url, deps_file, _ in sorted(self._deps_files)] + - ['']) # Ensure newline at end of file. + _VarsToLines(self._vars) + [ + '# %s, %s' % (url, deps_file) + for url, deps_file, _ in sorted(self._deps_files) + ] + ['']) # Ensure newline at end of file. def _add_dep(self, dep): """Helper to add a dependency to flattened DEPS. @@ -2546,6 +2562,8 @@ def CMDflatten(parser, args): '--pin-all-deps', action='store_true', help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so ' 'for checked out deps, NOT deps_os.')) + parser.add_option('--deps-graph-file', + help='Provide a path for the output graph file') options, args = parser.parse_args(args) options.nohooks = True @@ -2568,6 +2586,10 @@ def CMDflatten(parser, args): else: print(flattener.deps_string) + if options.deps_graph_file: + with open(options.deps_graph_file, 'w') as f: + f.write('\n'.join(flattener.deps_graph_lines)) + deps_files = [{'url': d[0], 'deps_file': d[1], 'hierarchy': d[2]} for d in sorted(flattener.deps_files)] if options.output_deps_files: @@ -2599,6 +2621,7 @@ def _AllowedHostsToLines(allowed_hosts): def _DepsToLines(deps): + # type: (Mapping[str, Dependency]) -> Sequence[str] """Converts |deps| dict to list of lines for output.""" if not deps: return [] @@ -2609,6 +2632,20 @@ def _DepsToLines(deps): return s +def _DepsToDotGraphLines(deps): + # type: (Mapping[str, Dependency]) -> Sequence[str] + """Converts |deps| dict to list of lines for dot graphs""" + if not deps: + return [] + graph_lines = ["digraph {\n\trankdir=\"LR\";"] + for _, dep in sorted(deps.items()): + line = dep.hierarchy(include_url=False, graphviz=True) + if line: + graph_lines.append("\t%s" % line) + graph_lines.append("}") + return graph_lines + + def _DepsOsToLines(deps_os): """Converts |deps_os| dict to list of lines for output.""" if not deps_os: diff --git a/tests/gclient_git_smoketest.py b/tests/gclient_git_smoketest.py index 4113f460a2..6f270e15d8 100644 --- a/tests/gclient_git_smoketest.py +++ b/tests/gclient_git_smoketest.py @@ -725,70 +725,70 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): self.assertEqual([ 'gclient_gn_args_file = "src/repo2/gclient.args"', 'gclient_gn_args = [\'false_var\', \'false_str_var\', \'true_var\', ' - '\'true_str_var\', \'str_var\', \'cond_var\']', + '\'true_str_var\', \'str_var\', \'cond_var\']', 'allowed_hosts = [', ' "' + self.git_base + '",', ']', '', 'deps = {', - ' # src -> src/repo2 -> foo/bar', + ' # "src" -> "src/repo2" -> "foo/bar"', ' "foo/bar": {', ' "url": "' + self.git_base + 'repo_3",', ' "condition": \'(repo2_false_var) and (true_str_var)\',', ' },', '', - ' # src', + ' # "src"', ' "src": {', ' "url": "' + self.git_base + 'repo_6",', ' },', '', - ' # src -> src/mac_repo', + ' # "src" -> "src/mac_repo"', ' "src/mac_repo": {', ' "url": "' + self.git_base + 'repo_5",', ' "condition": \'checkout_mac\',', ' },', '', - ' # src -> src/repo8 -> src/recursed_os_repo', + ' # "src" -> "src/repo8" -> "src/recursed_os_repo"', ' "src/recursed_os_repo": {', ' "url": "' + self.git_base + 'repo_5",', ' "condition": \'(checkout_linux) or (checkout_mac)\',', ' },', '', - ' # src -> src/repo15', + ' # "src" -> "src/repo15"', ' "src/repo15": {', ' "url": "' + self.git_base + 'repo_15",', ' },', '', - ' # src -> src/repo16', + ' # "src" -> "src/repo16"', ' "src/repo16": {', ' "url": "' + self.git_base + 'repo_16",', ' },', '', - ' # src -> src/repo2', + ' # "src" -> "src/repo2"', ' "src/repo2": {', - ' "url": "' + self.git_base + 'repo_2@%s",' % ( - self.githash('repo_2', 1)[:7]), + ' "url": "' + self.git_base + 'repo_2@%s",' % + (self.githash('repo_2', 1)[:7]), ' "condition": \'true_str_var\',', ' },', '', - ' # src -> src/repo4', + ' # "src" -> "src/repo4"', ' "src/repo4": {', ' "url": "' + self.git_base + 'repo_4",', ' "condition": \'False\',', ' },', '', - ' # src -> src/repo8', + ' # "src" -> "src/repo8"', ' "src/repo8": {', ' "url": "' + self.git_base + 'repo_8",', ' },', '', - ' # src -> src/unix_repo', + ' # "src" -> "src/unix_repo"', ' "src/unix_repo": {', ' "url": "' + self.git_base + 'repo_5",', ' "condition": \'checkout_linux\',', ' },', '', - ' # src -> src/win_repo', + ' # "src" -> "src/win_repo"', ' "src/win_repo": {', ' "url": "' + self.git_base + 'repo_5",', ' "condition": \'checkout_win\',', @@ -797,7 +797,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): '}', '', 'hooks = [', - ' # src', + ' # "src"', ' {', ' "pattern": ".",', ' "condition": \'True\',', @@ -806,11 +806,11 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "python",', ' "-c",', ' "open(\'src/git_hooked1\', \'w\')' - '.write(\'git_hooked1\')",', + '.write(\'git_hooked1\')",', ' ]', ' },', '', - ' # src', + ' # "src"', ' {', ' "pattern": "nonexistent",', ' "cwd": ".",', @@ -821,7 +821,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' ]', ' },', '', - ' # src', + ' # "src"', ' {', ' "pattern": ".",', ' "condition": \'checkout_mac\',', @@ -830,11 +830,11 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "python",', ' "-c",', ' "open(\'src/git_hooked_mac\', \'w\').write(' - '\'git_hooked_mac\')",', + '\'git_hooked_mac\')",', ' ]', ' },', '', - ' # src -> src/repo15', + ' # "src" -> "src/repo15"', ' {', ' "name": "absolute_cwd",', ' "pattern": ".",', @@ -846,7 +846,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' ]', ' },', '', - ' # src -> src/repo16', + ' # "src" -> "src/repo16"', ' {', ' "name": "relative_cwd",', ' "pattern": ".",', @@ -860,45 +860,45 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ']', '', 'vars = {', - ' # src', + ' # "src"', ' "DummyVariable": \'repo\',', '', - ' # src', + ' # "src"', ' "cond_var": \'false_str_var and true_var\',', '', - ' # src', + ' # "src"', ' "false_str_var": \'False\',', '', - ' # src', + ' # "src"', ' "false_var": False,', '', - ' # src', + ' # "src"', ' "git_base": \'' + self.git_base + '\',', '', - ' # src', + ' # "src"', ' "hook1_contents": \'git_hooked1\',', '', - ' # src -> src/repo2', + ' # "src" -> "src/repo2"', ' "repo2_false_var": \'False\',', '', - ' # src', + ' # "src"', ' "repo5_var": \'/repo_5\',', '', - ' # src', + ' # "src"', ' "str_var": \'abc\',', '', - ' # src', + ' # "src"', ' "true_str_var": \'True\',', '', - ' # src [custom_var override]', + ' # "src" [custom_var override]', ' "true_var": \'False\',', '', '}', '', '# ' + self.git_base + 'repo_15, DEPS', '# ' + self.git_base + 'repo_16, DEPS', - '# ' + self.git_base + 'repo_2@%s, DEPS' % ( - self.githash('repo_2', 1)[:7]), + '# ' + self.git_base + 'repo_2@%s, DEPS' % + (self.githash('repo_2', 1)[:7]), '# ' + self.git_base + 'repo_6, DEPS', '# ' + self.git_base + 'repo_8, DEPS', ], deps_contents.splitlines()) @@ -920,89 +920,89 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): self.assertEqual([ 'gclient_gn_args_file = "src/repo2/gclient.args"', 'gclient_gn_args = [\'false_var\', \'false_str_var\', \'true_var\', ' - '\'true_str_var\', \'str_var\', \'cond_var\']', + '\'true_str_var\', \'str_var\', \'cond_var\']', 'allowed_hosts = [', ' "' + self.git_base + '",', ']', '', 'deps = {', - ' # src -> src/repo2 -> foo/bar', + ' # "src" -> "src/repo2" -> "foo/bar"', ' "foo/bar": {', - ' "url": "' + self.git_base + 'repo_3@%s",' % ( - self.githash('repo_3', 2)), + ' "url": "' + self.git_base + 'repo_3@%s",' % + (self.githash('repo_3', 2)), ' "condition": \'(repo2_false_var) and (true_str_var)\',', ' },', '', - ' # src', + ' # "src"', ' "src": {', - ' "url": "' + self.git_base + 'repo_6@%s",' % ( - self.githash('repo_6', 1)), + ' "url": "' + self.git_base + 'repo_6@%s",' % + (self.githash('repo_6', 1)), ' },', '', - ' # src -> src/mac_repo', + ' # "src" -> "src/mac_repo"', ' "src/mac_repo": {', - ' "url": "' + self.git_base + 'repo_5@%s",' % ( - self.githash('repo_5', 3)), + ' "url": "' + self.git_base + 'repo_5@%s",' % + (self.githash('repo_5', 3)), ' "condition": \'checkout_mac\',', ' },', '', - ' # src -> src/repo8 -> src/recursed_os_repo', + ' # "src" -> "src/repo8" -> "src/recursed_os_repo"', ' "src/recursed_os_repo": {', - ' "url": "' + self.git_base + 'repo_5@%s",' % ( - self.githash('repo_5', 3)), + ' "url": "' + self.git_base + 'repo_5@%s",' % + (self.githash('repo_5', 3)), ' "condition": \'(checkout_linux) or (checkout_mac)\',', ' },', '', - ' # src -> src/repo15', + ' # "src" -> "src/repo15"', ' "src/repo15": {', - ' "url": "' + self.git_base + 'repo_15@%s",' % ( - self.githash('repo_15', 1)), + ' "url": "' + self.git_base + 'repo_15@%s",' % + (self.githash('repo_15', 1)), ' },', '', - ' # src -> src/repo16', + ' # "src" -> "src/repo16"', ' "src/repo16": {', - ' "url": "' + self.git_base + 'repo_16@%s",' % ( - self.githash('repo_16', 1)), + ' "url": "' + self.git_base + 'repo_16@%s",' % + (self.githash('repo_16', 1)), ' },', '', - ' # src -> src/repo2', + ' # "src" -> "src/repo2"', ' "src/repo2": {', - ' "url": "' + self.git_base + 'repo_2@%s",' % ( - self.githash('repo_2', 1)), + ' "url": "' + self.git_base + 'repo_2@%s",' % + (self.githash('repo_2', 1)), ' "condition": \'true_str_var\',', ' },', '', - ' # src -> src/repo4', + ' # "src" -> "src/repo4"', ' "src/repo4": {', - ' "url": "' + self.git_base + 'repo_4@%s",' % ( - self.githash('repo_4', 2)), + ' "url": "' + self.git_base + 'repo_4@%s",' % + (self.githash('repo_4', 2)), ' "condition": \'False\',', ' },', '', - ' # src -> src/repo8', + ' # "src" -> "src/repo8"', ' "src/repo8": {', - ' "url": "' + self.git_base + 'repo_8@%s",' % ( - self.githash('repo_8', 1)), + ' "url": "' + self.git_base + 'repo_8@%s",' % + (self.githash('repo_8', 1)), ' },', '', - ' # src -> src/unix_repo', + ' # "src" -> "src/unix_repo"', ' "src/unix_repo": {', - ' "url": "' + self.git_base + 'repo_5@%s",' % ( - self.githash('repo_5', 3)), + ' "url": "' + self.git_base + 'repo_5@%s",' % + (self.githash('repo_5', 3)), ' "condition": \'checkout_linux\',', ' },', '', - ' # src -> src/win_repo', + ' # "src" -> "src/win_repo"', ' "src/win_repo": {', - ' "url": "' + self.git_base + 'repo_5@%s",' % ( - self.githash('repo_5', 3)), + ' "url": "' + self.git_base + 'repo_5@%s",' % + (self.githash('repo_5', 3)), ' "condition": \'checkout_win\',', ' },', '', '}', '', 'hooks = [', - ' # src', + ' # "src"', ' {', ' "pattern": ".",', ' "condition": \'True\',', @@ -1011,11 +1011,11 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "python",', ' "-c",', ' "open(\'src/git_hooked1\', \'w\')' - '.write(\'git_hooked1\')",', + '.write(\'git_hooked1\')",', ' ]', ' },', '', - ' # src', + ' # "src"', ' {', ' "pattern": "nonexistent",', ' "cwd": ".",', @@ -1026,7 +1026,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' ]', ' },', '', - ' # src', + ' # "src"', ' {', ' "pattern": ".",', ' "condition": \'checkout_mac\',', @@ -1035,11 +1035,11 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "python",', ' "-c",', ' "open(\'src/git_hooked_mac\', \'w\').write(' - '\'git_hooked_mac\')",', + '\'git_hooked_mac\')",', ' ]', ' },', '', - ' # src -> src/repo15', + ' # "src" -> "src/repo15"', ' {', ' "name": "absolute_cwd",', ' "pattern": ".",', @@ -1051,7 +1051,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' ]', ' },', '', - ' # src -> src/repo16', + ' # "src" -> "src/repo16"', ' {', ' "name": "relative_cwd",', ' "pattern": ".",', @@ -1065,51 +1065,48 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ']', '', 'vars = {', - ' # src', + ' # "src"', ' "DummyVariable": \'repo\',', '', - ' # src', + ' # "src"', ' "cond_var": \'false_str_var and true_var\',', '', - ' # src', + ' # "src"', ' "false_str_var": \'False\',', '', - ' # src', + ' # "src"', ' "false_var": False,', '', - ' # src', + ' # "src"', ' "git_base": \'' + self.git_base + '\',', '', - ' # src', + ' # "src"', ' "hook1_contents": \'git_hooked1\',', '', - ' # src -> src/repo2', + ' # "src" -> "src/repo2"', ' "repo2_false_var": \'False\',', '', - ' # src', + ' # "src"', ' "repo5_var": \'/repo_5\',', '', - ' # src', + ' # "src"', ' "str_var": \'abc\',', '', - ' # src', + ' # "src"', ' "true_str_var": \'True\',', '', - ' # src', + ' # "src"', ' "true_var": True,', '', '}', '', - '# ' + self.git_base + 'repo_15@%s, DEPS' % ( - self.githash('repo_15', 1)), - '# ' + self.git_base + 'repo_16@%s, DEPS' % ( - self.githash('repo_16', 1)), - '# ' + self.git_base + 'repo_2@%s, DEPS' % ( - self.githash('repo_2', 1)), - '# ' + self.git_base + 'repo_6@%s, DEPS' % ( - self.githash('repo_6', 1)), - '# ' + self.git_base + 'repo_8@%s, DEPS' % ( - self.githash('repo_8', 1)), + '# ' + self.git_base + 'repo_15@%s, DEPS' % + (self.githash('repo_15', 1)), + '# ' + self.git_base + 'repo_16@%s, DEPS' % + (self.githash('repo_16', 1)), + '# ' + self.git_base + 'repo_2@%s, DEPS' % (self.githash('repo_2', 1)), + '# ' + self.git_base + 'repo_6@%s, DEPS' % (self.githash('repo_6', 1)), + '# ' + self.git_base + 'repo_8@%s, DEPS' % (self.githash('repo_8', 1)), ], deps_contents.splitlines()) # TODO(crbug.com/1024683): Enable for windows. @@ -1134,51 +1131,51 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): 'gclient_gn_args_file = "src/repo8/gclient.args"', "gclient_gn_args = ['str_var']", 'deps = {', - ' # src', + ' # "src"', ' "src": {', ' "url": "' + self.git_base + 'repo_10",', ' },', '', - ' # src -> src/repo9 -> src/repo8 -> src/recursed_os_repo', + ' # "src" -> "src/repo9" -> "src/repo8" -> "src/recursed_os_repo"', ' "src/recursed_os_repo": {', ' "url": "' + self.git_base + 'repo_5",', ' "condition": \'(checkout_linux) or (checkout_mac)\',', ' },', '', - ' # src -> src/repo11', + ' # "src" -> "src/repo11"', ' "src/repo11": {', ' "url": "' + self.git_base + 'repo_11",', ' "condition": \'(checkout_ios) or (checkout_mac)\',', ' },', '', - ' # src -> src/repo11 -> src/repo12', + ' # "src" -> "src/repo11" -> "src/repo12"', ' "src/repo12": {', ' "url": "' + self.git_base + 'repo_12",', ' "condition": \'(checkout_ios) or (checkout_mac)\',', ' },', '', - ' # src -> src/repo9 -> src/repo4', + ' # "src" -> "src/repo9" -> "src/repo4"', ' "src/repo4": {', ' "url": "' + self.git_base + 'repo_4",', ' "condition": \'checkout_android\',', ' },', '', - ' # src -> src/repo6', + ' # "src" -> "src/repo6"', ' "src/repo6": {', ' "url": "' + self.git_base + 'repo_6",', ' },', '', - ' # src -> src/repo9 -> src/repo7', + ' # "src" -> "src/repo9" -> "src/repo7"', ' "src/repo7": {', ' "url": "' + self.git_base + 'repo_7",', ' },', '', - ' # src -> src/repo9 -> src/repo8', + ' # "src" -> "src/repo9" -> "src/repo8"', ' "src/repo8": {', ' "url": "' + self.git_base + 'repo_8",', ' },', '', - ' # src -> src/repo9', + ' # "src" -> "src/repo9"', ' "src/repo9": {', ' "url": "' + self.git_base + 'repo_9",', ' },', @@ -1186,7 +1183,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): '}', '', 'vars = {', - ' # src -> src/repo9', + ' # "src" -> "src/repo9"', ' "str_var": \'xyz\',', '', '}', @@ -1230,12 +1227,12 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): self.assertEqual([ 'deps = {', - ' # src', + ' # "src"', ' "src": {', ' "url": "' + self.git_base + 'repo_14",', ' },', '', - ' # src -> src/another_cipd_dep', + ' # "src" -> src/another_cipd_dep', ' "src/another_cipd_dep": {', ' "packages": [', ' {', @@ -1250,7 +1247,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "dep_type": "cipd",', ' },', '', - ' # src -> src/cipd_dep', + ' # "src" -> src/cipd_dep', ' "src/cipd_dep": {', ' "packages": [', ' {', @@ -1261,7 +1258,7 @@ class GClientSmokeGIT(gclient_smoketest_base.GClientSmokeBase): ' "dep_type": "cipd",', ' },', '', - ' # src -> src/cipd_dep_with_cipd_variable', + ' # "src" -> src/cipd_dep_with_cipd_variable', ' "src/cipd_dep_with_cipd_variable": {', ' "packages": [', ' {',