| #!/usr/bin/env python3.8 |
| # 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) |