| #!/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. | |
| ''' | |
| Prints completions for GN build target names. | |
| Reads the GN build targets from //out/default/project_lite.json | |
| (generated by prepare_targets.py) and prints completions that match the search | |
| label. | |
| This can be invoked to generate completions in any shell that supports | |
| completions. | |
| For a description of GN labels, run `fx gn help labels`. | |
| ''' | |
| import argparse | |
| import collections | |
| import json | |
| import os | |
| import sys | |
| FUCHSIA_DIR = os.environ['FUCHSIA_DIR'] | |
| DEFAULT_PROJECT_FILE = 'project_lite.json' | |
| try: | |
| DEFAULT_OUT_DIR = os.environ['FUCHSIA_BUILD_DIR'] | |
| except KeyError: | |
| DEFAULT_OUT_DIR = os.path.join(FUCHSIA_DIR, 'out', 'default') | |
| class Completions(object): | |
| '''Provides shell-like completions for partial GN labels.''' | |
| def __init__(self, prefix): | |
| self._prefix = prefix | |
| def factory(): | |
| return collections.defaultdict(factory) | |
| self._children = factory() | |
| def prefix(self): | |
| return self._prefix | |
| def insert_label(self, label): | |
| '''Inserts the `label` into the `completions` dict. | |
| Args: | |
| label: A GN label that must start with `self.prefix()`. | |
| Raises: | |
| Exception: If `label` does not start with `self.prefix()`. | |
| ''' | |
| if not label.startswith(self._prefix): | |
| raise Exception('label "{}" must have the prefix "{}"'.format( | |
| label, self._prefix)) | |
| children = self._children | |
| for segment in split_label(label[len(self._prefix):]): | |
| children = children[segment] | |
| def list_completions(self): | |
| '''Yields partial GN label completions truncated at the first ambiguity. | |
| Eg. for a set of targets: | |
| ['foo/bar/baz:test', | |
| 'foo/bar/bat/bar:test', | |
| 'foo/bar/bat:bin', | |
| 'foo/bar:test'] | |
| the expected completions are: | |
| ['foo/bar/baz', | |
| 'foo/bar/bat', | |
| 'foo/bar:test'] | |
| Args: | |
| completions: The dictionary of completions. | |
| Yields: | |
| Partial GN labels that should be suggested as completions. | |
| ''' | |
| if not self._children: | |
| return | |
| label = '' | |
| children = self._children | |
| # Descend down the label segments until there is a choice | |
| # to make. These are the most useful completions. | |
| while len(children) == 1: | |
| segment, children = next(iter(children.items())) | |
| label += segment | |
| if not children: | |
| # There is only a single match to suggest. | |
| yield self._prefix + label | |
| for segment, _ in children.items(): | |
| yield self._prefix + label + segment | |
| def get_project_subdir(): | |
| '''Returns the current working subdirectory of FUCHSIA_DIR. | |
| Returns: | |
| A subdirectory without a leading '/'. | |
| Raises: | |
| Exception: if the current working directory is not a subdirectory | |
| of FUCHSIA_DIR. | |
| ''' | |
| cwd = os.getcwd() | |
| if not cwd.startswith(FUCHSIA_DIR): | |
| raise Exception('current working directory must be in {}'.format( | |
| FUCHSIA_DIR)) | |
| subdir = cwd[len(FUCHSIA_DIR):] | |
| if subdir.startswith('/'): | |
| subdir = subdir[1:] | |
| return subdir | |
| def list_matching_targets(project_file, prefix): | |
| '''Yields GN targets in `project_file` that match `prefix`. | |
| Args: | |
| project_file: A JSON file which contains a list of GN labels under the | |
| 'targets' key in the root object. | |
| prefix: The GN label prefix against which to match the list of targets. | |
| Returns: | |
| A generator that yields GN targets that match the GN label `prefix`. | |
| ''' | |
| for target in json.load(project_file)['targets']: | |
| if target.startswith(prefix): | |
| yield target | |
| def is_label_relative(label): | |
| '''Returns True if `label` is relative (starts with '//').''' | |
| return not label.startswith('//') | |
| def concat_labels(base, leaf): | |
| '''Concatenates two GN labels, removing extra '/' characters if needed. | |
| Args: | |
| base: A relative or absolute GN label which does not include a target | |
| (:foo). | |
| leaf: A relative GN label. | |
| Returns: | |
| A GN label that is the concatenation of `base` and `leaf`. The label is | |
| relative or absolute depending on the `base` label. | |
| Raises: | |
| Exception: If the `leaf` label is absolute or `base` contains a target | |
| (:foo). | |
| ''' | |
| if ':' in base: | |
| raise Exception('base label cannot contain a target: {}'.format(base)) | |
| if leaf.startswith('/'): | |
| raise Exception('leaf label cannot be absolute: {}'.format(leaf)) | |
| colon = leaf.startswith(':') | |
| if colon or not leaf or base.endswith('/'): | |
| return base + leaf | |
| return '{}/{}'.format(base, leaf) | |
| def make_label_relative(base, absolute): | |
| '''Makes `absolute` a GN label relative to `base`. | |
| Args: | |
| base: An absolute GN label which does not include a target (:foo). | |
| absolute: An absolute GN label of which `base` is a prefix. | |
| Returns: | |
| A GN label relative to `base` that refers to the same label as `absolute`. | |
| ''' | |
| relative = absolute[len(base):] | |
| if relative.startswith('/'): | |
| relative = relative[1:] | |
| return relative | |
| def split_label(label): | |
| '''Splits a potentially partial GN label into segments. | |
| The segments are meant to be joined without a separator, which makes handling | |
| certain edge cases easier. Each delimiter ('/', ':') is included in the | |
| segment that follows it. Eg 'foo/bar:baz' => ['foo', '/bar', ':baz']. | |
| Absolute GN labels always start with the '//' segment. | |
| Toolchains are kept together with the target segment. | |
| Args: | |
| label: A potentially partial GN label, relative or absolute. | |
| Returns: | |
| A list of segments that can be joined without a separator to re-create | |
| the original label. | |
| ''' | |
| if not label: | |
| return [''] | |
| toolchain = '' | |
| if label.endswith(')'): | |
| # This label contains a toolchain, which looks like :baz(//toolchain). | |
| toolchain_idx = label.rindex('(') | |
| toolchain = label[toolchain_idx:] | |
| label = label[0:toolchain_idx] | |
| leaf = '' | |
| leaf_idx = label.find(':') | |
| if leaf_idx >= 0: | |
| leaf = label[leaf_idx:] | |
| label = label[0:leaf_idx] | |
| segments = [] | |
| if label.startswith('//'): | |
| segments = ['//'] | |
| label = label[2:] | |
| while True: | |
| # Always search after the last /. | |
| idx = label.find('/', 1) | |
| if idx > 0: | |
| segments.append(label[0:idx]) | |
| label = label[idx:] | |
| else: | |
| if label: | |
| segments.append(label) | |
| break | |
| if leaf: | |
| segments.append(leaf) | |
| if toolchain: | |
| if segments: | |
| segments[-1] += toolchain | |
| else: | |
| segments = [toolchain] | |
| return segments | |
| def main(args): | |
| base_label = '' | |
| prefix = args.search_label | |
| relative = is_label_relative(args.search_label) | |
| if relative: | |
| base_label = '//{}'.format(get_project_subdir()) | |
| prefix = concat_labels(base_label, args.search_label) | |
| eprint('Base: {}'.format(base_label)) | |
| eprint('Search: {}'.format(args.search_label)) | |
| eprint('Prefix: {}'.format(prefix)) | |
| completions = Completions(args.search_label) | |
| with open(os.path.join(args.build_dir, args.project_file)) as fin: | |
| for target in list_matching_targets(fin, prefix): | |
| if relative: | |
| target = make_label_relative(base_label, target) | |
| completions.insert_label(target) | |
| for completion in completions.list_completions(): | |
| print(completion) | |
| def eprint(*args, **kwargs): | |
| print(*args, file=sys.stderr, **kwargs) | |
| if __name__ == '__main__': | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument('--build_dir', default=DEFAULT_OUT_DIR) | |
| p.add_argument('--project_file', default=DEFAULT_PROJECT_FILE) | |
| p.add_argument('search_label', nargs='?', default='') | |
| try: | |
| main(p.parse_args()) | |
| except Exception as e: | |
| eprint('error: {}'.format(e)) | |
| sys.exit(1) |