blob: 7e349507619b271c3d6bd98ba5a41cbfa2885229 [file] [log] [blame] [edit]
#!/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).
See https://fxbug.dev/134999 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()
# 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):
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]
def printerr(msg, **kwargs) -> int:
"""Like print() but sends output to sys.stderr by default, then return 1."""
if "file" in kwargs:
print("ERROR: " + msg, **kwargs)
else:
print("ERROR: " + msg, file=sys.stderr, **kwargs)
return 1
def get_build_dir(fuchsia_dir: Path) -> Path:
"""Get current Ninja build directory."""
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 main(main_args: Sequence[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
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.",
)
subparsers = parser.add_subparsers(required=True, help="sub-command help")
print_parser = subparsers.add_parser(
"print", help="Print build API module content"
)
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 modules"
)
list_parser.set_defaults(func=cmd_list)
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",
)
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:]))