| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 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. |
| |
| """A tool providing information about the Fuchsia build graph(s). |
| |
| This is not intended to be called directly by developers, but by |
| specialized tools and scripts like `fx`, `ffx` and others. |
| |
| See https://fxbug.dev/42084664 for context. |
| """ |
| |
| # TECHNICAL NOTE: Reduce imports to a strict minimum to keep startup time |
| # of this script as low a possible. You can always perform an import lazily |
| # only when you need it (e.g. see how json and difflib are imported below). |
| import argparse |
| import os |
| import sys |
| from pathlib import Path |
| from typing import List, Sequence |
| |
| _SCRIPT_FILE = Path(__file__) |
| _SCRIPT_DIR = _SCRIPT_FILE.parent |
| _FUCHSIA_DIR = (_SCRIPT_DIR / ".." / "..").resolve() |
| sys.path.insert(0, _SCRIPT_DIR) |
| |
| |
| def _get_host_platform() -> str: |
| """Return host platform name, following Fuchsia conventions.""" |
| if sys.platform == "linux": |
| return "linux" |
| elif sys.platform == "darwin": |
| return "mac" |
| else: |
| return os.uname().sysname |
| |
| |
| def _get_host_arch() -> str: |
| """Return host CPU architecture, following Fuchsia conventions.""" |
| host_arch = os.uname().machine |
| if host_arch == "x86_64": |
| return "x64" |
| elif host_arch.startswith(("armv8", "aarch64")): |
| return "arm64" |
| else: |
| return host_arch |
| |
| |
| def _get_host_tag() -> str: |
| """Return host tag, following Fuchsia conventions.""" |
| return "%s-%s" % (get_host_platform(), get_host_arch()) |
| |
| |
| def _warning(msg: str): |
| """Print a warning message to stderr.""" |
| if sys.stderr.isatty(): |
| print(f"\033[1;33mWARNING:\033[0m {msg}", file=sys.stderr) |
| else: |
| print(f"WARNING: {msg}", file=sys.stderr) |
| |
| |
| def _error(msg: str): |
| """Print an error message to stderr.""" |
| if sys.stderr.isatty(): |
| print(f"\033[1;31mERROR:\033[0m {msg}", file=sys.stderr) |
| else: |
| print(f"ERROR: {msg}", file=sys.stderr) |
| |
| |
| def _printerr(msg) -> int: |
| """Like _error() but returns 1.""" |
| _error(msg) |
| return 1 |
| |
| |
| # NOTE: Do not use dataclasses because its import adds 20ms of startup time |
| # which is _massive_ here. |
| class BuildApiModule: |
| """Simple dataclass-like type describing a given build API module.""" |
| |
| def __init__(self, name: str, file_path: Path): |
| self.name = name |
| self.path = file_path |
| |
| def get_content(self): |
| """Return content as sttring.""" |
| return self.path.read_text() |
| |
| def get_content_as_json(self) -> object: |
| """Return content as a JSON object + lazy-loads the 'json' module.""" |
| import json |
| |
| return json.load(self.path.open()) |
| |
| |
| class BuildApiModuleList(object): |
| """Models the list of all build API module files.""" |
| |
| def __init__(self, build_dir: Path): |
| self._modules: List[BuildApiModule] = [] |
| self.list_path = build_dir / "build_api_client_info" |
| if not self.list_path.exists(): |
| return |
| |
| for line in self.list_path.read_text().splitlines(): |
| name, equal, file_path = line.partition("=") |
| assert equal == "=", f"Invalid format for input file: {list_path}" |
| self._modules.append(BuildApiModule(name, build_dir / file_path)) |
| |
| self._modules.sort(key=lambda x: x.name) # Sort by name. |
| self._map = {m.name: m for m in self._modules} |
| |
| def empty(self) -> bool: |
| """Return True if modules list is empty.""" |
| return len(self._modules) == 0 |
| |
| def modules(self) -> Sequence[BuildApiModule]: |
| """Return the sequence of BuildApiModule instances, sorted by name.""" |
| return self._modules |
| |
| def find(self, name: str) -> BuildApiModule: |
| """Find a BuildApiModule by name, return None if not found.""" |
| return self._map.get(name) |
| |
| def names(self) -> Sequence[str]: |
| """Return the sorted list of build API module names.""" |
| return [m.name for m in self._modules] |
| |
| |
| class OutputsDatabase(object): |
| """Manage a lazily-created / updated NinjaOutputsTabular database. |
| |
| Usage is: |
| 1) Create instance. |
| 2) Call load() to load the database from the Ninja build directory. |
| 3) Call gn_label_to_paths() or path_to_gn_label() as many times |
| as needed. |
| """ |
| |
| def __init__(self): |
| self._database = None |
| |
| def load(self, build_dir: Path) -> bool: |
| """Load the database from the given build directory. |
| |
| This takes care of converting the ninja_outputs.json file generated |
| by GN into the more efficient tabular format, whenever this is needed. |
| |
| Args: |
| build_dir: Ninja build directory. |
| |
| Returns: |
| On success return True, on failure, print an error message to stderr |
| then return False. |
| """ |
| json_file = build_dir / "ninja_outputs.json" |
| tab_file = build_dir / "ninja_outputs.tabular" |
| if not json_file.exists(): |
| if tab_file.exists(): |
| tab_file.unlink() |
| print( |
| f"ERROR: Missing Ninja outputs file: {json_file}", |
| file=sys.stderr, |
| ) |
| return False |
| |
| from gn_ninja_outputs import NinjaOutputsTabular as OutputsDatabase |
| |
| self._database = OutputsDatabase() |
| |
| if ( |
| not tab_file.exists() |
| or tab_file.stat().st_mtime < json_file.stat().st_mtime |
| ): |
| # Re-generate database file when needed |
| self._database.load_from_json(json_file) |
| self._database.save_to_file(tab_file) |
| else: |
| # Load previously generated database. |
| self._database.load_from_file(tab_file) |
| return True |
| |
| def gn_label_to_paths(self, label: str) -> List[str]: |
| return self._database.gn_label_to_paths(label) |
| |
| def path_to_gn_label(self, path: str) -> str: |
| return self._database.path_to_gn_label(path) |
| |
| |
| def get_build_dir(fuchsia_dir: Path) -> Path: |
| """Get current Ninja build directory.""" |
| # Use $FUCHSIA_DIR/.fx-build-dir if present. This is only useful |
| # when invoking the script directly from the command-line, i.e. |
| # during build system development. |
| # |
| # `fx` scripts should use the `fx-build-api-client` function which |
| # always sets --build-dir to the appropriate value instead |
| # (https://fxbug.dev/336720162). |
| file = fuchsia_dir / ".fx-build-dir" |
| if not file.exists(): |
| return Path() |
| return fuchsia_dir / file.read_text().strip() |
| |
| |
| def cmd_list(args: argparse.Namespace) -> int: |
| """Implement the `list` command.""" |
| for name in args.modules.names(): |
| print(name) |
| return 0 |
| |
| |
| def cmd_print(args: argparse.Namespace) -> int: |
| """Implement the `print` command.""" |
| module = args.modules.find(args.api_name) |
| if not module: |
| return _printerr( |
| f"Unknown build API module name {args.api_name}, must be one of:\n\n %s\n" |
| % "\n ".join(args.modules.names()) |
| ) |
| |
| if not module.path.exists(): |
| return _printerr( |
| f"Missing input file, please use `fx set` or `fx gen` command: {module.path}" |
| ) |
| |
| print(module.get_content()) |
| return 0 |
| |
| |
| def cmd_print_all(args: argparse.Namespace) -> int: |
| """Implement the `print_all` command.""" |
| result = {} |
| for module in args.modules.modules(): |
| if module.name != "api": |
| result[module.name] = { |
| "file": os.path.relpath(module.path, args.build_dir), |
| "json": module.get_content_as_json(), |
| } |
| import json |
| |
| if args.pretty: |
| print( |
| json.dumps(result, sort_keys=True, indent=2, separators=(",", ": ")) |
| ) |
| else: |
| print(json.dumps(result, sort_keys=True)) |
| return 0 |
| |
| |
| def cmd_ninja_path_to_gn_label(args: argparse.Namespace) -> int: |
| """Implement the `ninja_path_to_gn_label` command.""" |
| outputs = OutputsDatabase() |
| if not outputs.load(args.build_dir): |
| return 1 |
| |
| failure = False |
| labels = set() |
| for path in args.paths: |
| label = outputs.path_to_gn_label(path) |
| if label: |
| labels.add(label) |
| continue |
| |
| if args.allow_unknown and not path.startswith("/"): |
| labels.add(path) |
| continue |
| |
| print( |
| f"ERROR: Unknown Ninja target path: {path}", |
| file=sys.stderr, |
| ) |
| failure = True |
| |
| if failure: |
| return 1 |
| |
| print("\n".join(sorted(labels))) |
| return 0 |
| |
| |
| def _get_target_cpu(build_dir: Path) -> str: |
| args_json_path = build_dir / "args.json" |
| if not args_json_path.exists(): |
| return "unknown_cpu" |
| import json |
| |
| args_json = json.load(args_json_path.open()) |
| if not isinstance(args_json, dict): |
| return "unknown_cpu" |
| return args_json.get("target_cpu", "unknown_cpu") |
| |
| |
| def cmd_gn_label_to_ninja_paths(args: argparse.Namespace) -> int: |
| """Implement the `gn_label_to_ninja_paths` command.""" |
| outputs = OutputsDatabase() |
| if not outputs.load(args.build_dir): |
| return 1 |
| |
| from gn_labels import GnLabelQualifier |
| |
| host_cpu = args.host_tag.split("-")[1] |
| target_cpu = _get_target_cpu(args.build_dir) |
| qualifier = GnLabelQualifier(host_cpu, target_cpu) |
| |
| failure = False |
| all_paths = [] |
| for label in args.labels: |
| if label.startswith("//"): |
| qualified_label = qualifier.qualify_label(label) |
| paths = outputs.gn_label_to_paths(qualified_label) |
| if paths: |
| all_paths.extend(paths) |
| continue |
| _error(f"Unknown GN label (not in the configured graph): {label}") |
| failure = True |
| elif label.startswith("/"): |
| _error( |
| f"Absolute path is not a valid GN label or Ninja path: {label}" |
| ) |
| failure = True |
| elif args.allow_unknown: |
| # Assume this is a Ninja path. |
| all_paths.append(label) |
| else: |
| _error(f"Not a proper GN label (must start with //): {label}") |
| failure = True |
| |
| if failure: |
| return 1 |
| |
| for path in sorted(all_paths): |
| print(path) |
| return 0 |
| |
| |
| def cmd_fx_build_args_to_labels(args: argparse.Namespace) -> int: |
| outputs = OutputsDatabase() |
| if not outputs.load(args.build_dir): |
| return 1 |
| |
| from gn_labels import GnLabelQualifier |
| |
| host_cpu = args.host_tag.split("-")[1] |
| target_cpu = _get_target_cpu(args.build_dir) |
| qualifier = GnLabelQualifier(host_cpu, target_cpu) |
| |
| failure = False |
| |
| def ninja_path_to_gn_label(path: str) -> str: |
| label = outputs.path_to_gn_label(path) |
| if label: |
| label_args = qualifier.label_to_build_args(label) |
| _warning( |
| f"Use '{' '.join(label_args)}' instead of Ninja path '{path}'" |
| ) |
| return label |
| |
| if args.allow_unknown and not path.startswith("/"): |
| return path |
| |
| _error(f"Unknown Ninja path: {path}") |
| nonlocal failure |
| failure = True |
| return "" |
| |
| qualifier.set_ninja_path_to_gn_label(ninja_path_to_gn_label) |
| |
| labels = qualifier.build_args_to_labels(args.args) |
| if failure: |
| return 1 |
| |
| for label in labels: |
| print(label) |
| |
| return 0 |
| |
| |
| def main(main_args: Sequence[str]) -> int: |
| parser = argparse.ArgumentParser( |
| description=__doc__, formatter_class=argparse.RawTextHelpFormatter |
| ) |
| parser.add_argument( |
| "--fuchsia-dir", |
| default=_FUCHSIA_DIR, |
| type=Path, |
| help="Specify Fuchsia source directory.", |
| ) |
| parser.add_argument( |
| "--build-dir", |
| type=Path, |
| help="Specify Ninja build directory.", |
| ) |
| parser.add_argument( |
| "--host-tag", |
| help="Host platform tag, using Fuchsia conventions (auto-detected).", |
| # NOTE: Do not set a default with _get_host_tag() here for fastser startup, |
| # since the //build/api/client wrapper script will always set this option. |
| ) |
| subparsers = parser.add_subparsers(required=True, help="sub-command help.") |
| print_parser = subparsers.add_parser( |
| "print", |
| help="Print build API module content.", |
| description="Print the content of a given build API module, given its name. " |
| "Use the 'list' command to print the list of all available names.", |
| ) |
| print_parser.add_argument("api_name", help="Name of build API module.") |
| print_parser.set_defaults(func=cmd_print) |
| |
| print_all_parser = subparsers.add_parser( |
| "print_all", |
| help="Print single JSON containing the content of all build API modules.", |
| ) |
| print_all_parser.add_argument( |
| "--pretty", action="store_true", help="Pretty print the JSON output." |
| ) |
| print_all_parser.set_defaults(func=cmd_print_all) |
| |
| list_parser = subparsers.add_parser( |
| "list", |
| help="Print list of all build API module names.", |
| description="Print list of all build API module names.", |
| ) |
| list_parser.set_defaults(func=cmd_list) |
| |
| ninja_path_to_gn_label_parser = subparsers.add_parser( |
| "ninja_path_to_gn_label", |
| help="Print the GN label of a given Ninja output path.", |
| ) |
| ninja_path_to_gn_label_parser.add_argument( |
| "--allow-unknown", |
| action="store_true", |
| help="Keep unknown input Nija paths in result.", |
| ) |
| ninja_path_to_gn_label_parser.add_argument( |
| "paths", |
| metavar="NINJA_PATH", |
| nargs="+", |
| help="Ninja output path, relative to the build directory.", |
| ) |
| ninja_path_to_gn_label_parser.set_defaults(func=cmd_ninja_path_to_gn_label) |
| |
| gn_label_to_ninja_paths_parser = subparsers.add_parser( |
| "gn_label_to_ninja_paths", |
| help="Print the Ninja output paths of one or more GN labels.", |
| description="Print the Ninja output paths of one or more GN labels.", |
| ) |
| gn_label_to_ninja_paths_parser.add_argument( |
| "--allow-unknown", |
| action="store_true", |
| help="Keep unknown input GN labels in result.", |
| ) |
| gn_label_to_ninja_paths_parser.add_argument( |
| "labels", |
| metavar="GN_LABEL", |
| nargs="+", |
| help="A qualified GN label (begins with //, may include full toolchain suffix).", |
| ) |
| gn_label_to_ninja_paths_parser.set_defaults( |
| func=cmd_gn_label_to_ninja_paths |
| ) |
| |
| fx_build_args_to_labels_parser = subparsers.add_parser( |
| "fx_build_args_to_labels", |
| help="Parse fx build arguments into qualified GN labels.", |
| description="Convert a series of `fx build` arguments into a list of fully qualified GN labels.", |
| ) |
| fx_build_args_to_labels_parser.add_argument( |
| "--allow-unknown", action="store_true" |
| ) |
| fx_build_args_to_labels_parser.add_argument( |
| "--args", required=True, nargs=argparse.REMAINDER |
| ) |
| fx_build_args_to_labels_parser.set_defaults( |
| func=cmd_fx_build_args_to_labels |
| ) |
| |
| args = parser.parse_args(main_args) |
| |
| if not args.build_dir: |
| args.build_dir = get_build_dir(args.fuchsia_dir) |
| |
| if not args.build_dir.exists(): |
| return _printerr( |
| "Could not locate build directory, please use `fx set` command or use --build-dir=DIR.", |
| ) |
| |
| if not args.host_tag: |
| args.host_tag = get_host_tag() |
| |
| args.modules = BuildApiModuleList(args.build_dir) |
| if args.modules.empty(): |
| return _printerr( |
| f"Missing input file, did you run `fx gen` or `fx set`?: {args.modules.list_path}" |
| ) |
| |
| return args.func(args) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |