| #!/usr/bin/env python3 |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import collections |
| import os |
| import subprocess |
| import sys |
| |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| FUCHSIA_ROOT = os.path.dirname( # $root |
| os.path.dirname( # scripts |
| SCRIPT_DIR)) # unification |
| |
| |
| class BuildGraph: |
| |
| def __init__(self, deps): |
| self._deps = deps |
| self._compute_refs() |
| |
| def _compute_refs(self): |
| self._nodes = set(self._deps.keys()) |
| self._nodes.update(set(dep for v in self._deps.values() for dep in v)) |
| refs = {} |
| |
| for node in self._nodes: |
| if node not in refs: |
| refs[node] = [] |
| if node not in self._deps: |
| self._deps[node] = [] |
| |
| for label, label_deps in self._deps.items(): |
| if label not in refs: |
| refs[label] = [] |
| for dep in label_deps: |
| if dep not in refs: |
| refs[dep] = [] |
| refs[dep].append(label) |
| |
| self._refs = refs |
| |
| def collapse_nodes(self, transform): |
| ''' |
| Rename each node in the graph by passing it through the transform function, |
| and merge nodes with the same name. The new node will have the union of |
| edges on the old nodes. Nodes for which the transform function returns |
| None will be deleted. |
| ''' |
| new_graph = collections.defaultdict(set) |
| for src, dsts in self._deps.items(): |
| xsrc = transform(src) |
| if xsrc is None: |
| continue |
| src_set = new_graph[xsrc] |
| for dst in dsts: |
| xdst = transform(dst) |
| if xdst is None: |
| continue |
| if xsrc == xdst: |
| continue |
| src_set.add(xdst) |
| self._deps = dict(new_graph) |
| self._compute_refs() |
| |
| def references(self, label): |
| return self._refs.get(label) |
| |
| def dependencies(self, label): |
| return self._deps.get(label) |
| |
| |
| def load_build_graph(gn_binary, zircon_dir, build_dir): |
| root_label = '//:default' |
| output = subprocess.check_output( |
| [ |
| gn_binary, '--root=' + zircon_dir, 'desc', build_dir, root_label, |
| 'deps', '--tree' |
| ], |
| encoding='utf8') |
| |
| # GN outputs the graph in an indented tree format: |
| # //foo |
| # //bar |
| # //baz |
| # //bar... |
| # The ... means this label has already been visited |
| |
| deps = collections.defaultdict(list) |
| label_stack = [root_label] |
| for line in output.splitlines(): |
| label = line.lstrip() |
| leading_spaces = len(line) - len(label) |
| assert leading_spaces >= 0 |
| assert leading_spaces % 2 == 0 |
| if label.endswith('...'): |
| label = label[:-3] |
| label_depth = leading_spaces // 2 |
| del label_stack[label_depth + 1:] |
| label_stack.append(label) |
| deps[label_stack[label_depth]].append(label) |
| |
| return BuildGraph(deps) |
| |
| |
| def lib_label(type, name): |
| return '//system/{}/{}'.format(type, name) |
| |
| |
| def format_label(label): |
| if label.startswith('//system/'): |
| label = label[len('//system/'):] |
| return label |
| |
| |
| def find_libraries(zircon_dir, type): |
| base = os.path.join(zircon_dir, 'system', type) |
| |
| def has_zn_build_file(dir): |
| build_path = os.path.join(base, dir, 'BUILD.gn') |
| if not os.path.isfile(build_path): |
| return False |
| with open(build_path, 'r') as build_file: |
| content = build_file.read() |
| return ( |
| '//build/unification/zx_library.gni' not in content and |
| '//build/unification/fidl_alias.gni' not in content) |
| |
| for _, dirs, _ in os.walk(base): |
| return [lib_label(type, dir) for dir in dirs if has_zn_build_file(dir)] |
| |
| |
| def unblocked_by(graph, label): |
| deps = graph.dependencies(label) |
| unblocked = [] |
| if deps is None: |
| return [] |
| for dep in deps: |
| if graph.references(dep) == [label]: |
| unblocked.append(dep) |
| return unblocked |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| 'Determines whether libraries can be ' |
| 'moved out of the ZN build') |
| parser.add_argument( |
| '--build-dir', |
| help='Path to the ZN build dir', |
| default=os.path.join(FUCHSIA_ROOT, 'out', 'default.zircon')) |
| parser.add_argument( |
| '--plain', |
| help='Only print library names, no dependency information', |
| action='store_true') |
| type = parser.add_mutually_exclusive_group(required=True) |
| type.add_argument( |
| '--banjo', help='Inspect Banjo libraries', action='store_true') |
| type.add_argument( |
| '--fidl', help='Inspect FIDL libraries', action='store_true') |
| type.add_argument( |
| '--ulib', help='Inspect C/C++ libraries', action='store_true') |
| type.add_argument( |
| '--devlib', |
| help='Inspect C/C++ libraries under dev', |
| action='store_true') |
| parser.add_argument( |
| 'name', |
| help='Name of the library to inspect; if empty, scan ' |
| 'all libraries of the given type', |
| nargs='?') |
| args = parser.parse_args() |
| |
| source_dir = FUCHSIA_ROOT |
| zircon_dir = os.path.join(source_dir, 'zircon') |
| build_dir = os.path.abspath(args.build_dir) |
| |
| if sys.platform.startswith('linux'): |
| platform = 'linux-x64' |
| elif sys.platform.startswith('darwin'): |
| platform = 'mac-x64' |
| else: |
| print('Unsupported platform: %s' % sys.platform) |
| return 1 |
| gn_binary = os.path.join( |
| source_dir, 'prebuilt', 'third_party', 'gn', platform, 'gn') |
| |
| graph = load_build_graph(gn_binary, zircon_dir, build_dir) |
| |
| def strip_toolchain_and_label(label): |
| dirname, _, _ = label.partition(':') |
| for type in ['fidl', 'banjo', 'ulib', 'dev/lib']: |
| if dirname == '//system/' + type: |
| return |
| return dirname |
| |
| graph.collapse_nodes(strip_toolchain_and_label) |
| |
| if args.fidl: |
| type = 'fidl' |
| elif args.banjo: |
| type = 'banjo' |
| elif args.ulib: |
| type = 'ulib' |
| elif args.devlib: |
| type = 'dev/lib' |
| |
| # Case 1: a library name is given. |
| if args.name: |
| name = args.name |
| if args.fidl: |
| # FIDL library names use the dot separator, but folders use an |
| # hyphen: be nice to users and support both forms. |
| name = name.replace('.', '-') |
| |
| refs = graph.references(lib_label(type, name)) |
| |
| if refs is None: |
| print('Could not find "%s", please check spelling!' % args.name) |
| return 1 |
| elif len(refs): |
| print('Nope, there are still references in the ZN build:') |
| shown = set() |
| batch = set(refs) |
| while batch: |
| for ref in sorted(batch): |
| print(' ' + format_label(ref)) |
| shown.update(batch) |
| new_batch = set() |
| for ref in batch: |
| for new_ref in graph.references(ref): |
| if new_ref not in shown: |
| new_batch.add(new_ref) |
| batch = new_batch |
| if batch: |
| print('---') |
| return 2 |
| else: |
| print('Yes you can!') |
| |
| return 0 |
| |
| # Case 2: no library name given. |
| libs = find_libraries(zircon_dir, type) |
| movable = set() |
| for lib in libs: |
| refs = graph.references(lib) |
| if not refs: |
| movable.add(lib) |
| if movable: |
| if not args.plain: |
| print('These libraries are free to go:') |
| for label in sorted(movable): |
| print(' ' + format_label(label)) |
| if args.plain: |
| continue |
| unblocked = unblocked_by(graph, label) |
| if unblocked: |
| print( |
| ' would unblock:', |
| ', '.join(format_label(u) for u in unblocked)) |
| if not graph.dependencies(label): |
| print(' no dependencies') |
| |
| else: |
| print('No library may be moved') |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |