blob: 53e8d302eb0e835e23dffb5b64cc34511e40da0d [file] [log] [blame]
#!/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
import typing as T
from pathlib import Path
_SCRIPT_FILE = Path(__file__)
_SCRIPT_DIR = _SCRIPT_FILE.parent
_FUCHSIA_DIR = (_SCRIPT_DIR / ".." / "..").resolve()
sys.path.insert(0, str(_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) -> None:
"""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) -> None:
"""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: str) -> 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) -> str:
"""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: T.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: {self.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) -> T.Sequence[BuildApiModule]:
"""Return the sequence of BuildApiModule instances, sorted by name."""
return self._modules
def find(self, name: str) -> BuildApiModule | None:
"""Find a BuildApiModule by name, return None if not found."""
return self._map.get(name)
def names(self) -> T.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) -> None:
self._database: None | OutputsDatabase = 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
database = OutputsDatabase()
self._database = database
if (
not tab_file.exists()
or tab_file.stat().st_mtime < json_file.stat().st_mtime
):
# Re-generate database file when needed
database.load_from_json(json_file)
database.save_to_file(tab_file)
else:
# Load previously generated database.
database.load_from_file(tab_file)
return True
def gn_label_to_paths(self, label: str) -> T.List[str]:
assert self._database
return self._database.gn_label_to_paths(label)
def path_to_gn_label(self, path: str) -> str:
assert self._database
return self._database.path_to_gn_label(path)
def target_name_to_gn_labels(self, target: str) -> T.List[str]:
assert self._database
return self._database.target_name_to_gn_labels(target)
def is_valid_target_name(self, target: str) -> bool:
assert self._database
return self._database.is_valid_target_name(target)
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 get_ninja_path(fuchsia_dir: Path, host_tag: str) -> Path:
return (
fuchsia_dir / "prebuilt" / "third_party" / "ninja" / host_tag / "ninja"
)
def cmd_list(args: argparse.Namespace) -> int:
"""Implement the `list` command."""
for name in args.modules.names():
print(name)
return 0
class LastBuildApiFilter(object):
"""Filter one or more build API modules based on last build artifacts."""
@staticmethod
def add_parser_arguments(parser: argparse.ArgumentParser) -> None:
"""Add parser arguments related to filtering the output."""
parser.add_argument(
"--last-build-only",
action="store_true",
help="Only include values matching the targets from the last build invocation.",
)
parser.add_argument(
"--no-last-build-check",
action="store_true",
help="When --last-build-only is used, ignore last build success check.",
)
@staticmethod
def has_filter_flag(args: argparse.Namespace) -> bool:
"""Returns True if |args| contains a filtering flag."""
return bool(args.last_build_only)
def __init__(self, args: argparse.Namespace):
self._error = ""
# Verify that the last build was successful, and set error otherwise.
if not args.no_last_build_check:
if not (args.build_dir / "last_ninja_build_success.stamp").exists():
self._error = "Last build did not complete successfully (use --ignore-last-build-check to ignore)."
return
self._ninja = get_ninja_path(args.fuchsia_dir, args.host_tag)
self._filter = self.generate_filter(self._ninja, args.build_dir)
@property
def error(self) -> str:
return self._error
def filter_json(self, api_name: str, json_value: T.Any) -> T.Any:
assert not self._error, "Cannot call filter_json() on error"
return self._filter.filter_api_json(api_name, json_value)
@staticmethod
def generate_filter(ninja: Path, build_dir: Path) -> T.Any:
import ninja_artifacts
from build_api_filter import BuildApiFilter
ninja_runner = ninja_artifacts.NinjaRunner(ninja)
last_build_artifacts = ninja_artifacts.get_last_build_artifacts(
build_dir, ninja_runner
)
last_build_sources = ninja_artifacts.get_last_build_sources(
build_dir, ninja_runner
)
return BuildApiFilter(last_build_artifacts, last_build_sources)
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}"
)
content = module.get_content()
if LastBuildApiFilter.has_filter_flag(args):
import json
api_filter = LastBuildApiFilter(args)
if api_filter.error:
print(f"ERROR: {api_filter.error}", file=sys.stderr)
return 1
json_content = api_filter.filter_json(
args.api_name, json.loads(content)
)
content = json.dumps(json_content, indent=2, separators=(",", ": "))
print(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 LastBuildApiFilter.has_filter_flag(args):
api_filter = LastBuildApiFilter(args)
if api_filter.error:
print(f"ERROR: {api_filter.error}", file=sys.stderr)
return 1
for name, v in result.items():
v["json"] = api_filter.filter_json(name, v["json"])
if args.pretty:
print(
json.dumps(result, sort_keys=True, indent=2, separators=(",", ": "))
)
else:
print(json.dumps(result, sort_keys=True))
return 0
class DebugSymbolCommandState(object):
def __init__(
self,
modules: BuildApiModuleList,
ninja: Path,
build_dir: Path,
resolve_build_ids: bool = True,
keep_duplicates: bool = False,
test_mode: bool = False,
) -> None:
import debug_symbols
self._modules = modules
self._ninja = ninja
self._build_dir = build_dir
self._debug_parser = debug_symbols.DebugSymbolsManifestParser(build_dir)
self._merge_duplicates = not keep_duplicates
if resolve_build_ids:
self._debug_parser.enable_build_id_resolution()
if test_mode:
# --test-mode is used during regression testing to avoid
# using a fake ELF input file. Simply return the file name
# as the build-id value for now.
def get_build_id(path: Path) -> str:
return path.name
self._debug_parser.set_build_id_callback_for_test(get_build_id)
def parse_manifest(self, last_build_only: bool) -> int:
module = self._modules.find("debug_symbols")
assert module
if not module.path.exists():
return _printerr(
f"Missing input file, please use `fx set` or `fx gen` command: {module.path}"
)
import json
with module.path.open("rt") as f:
manifest_json = json.load(f)
if last_build_only:
# Only filter the top-level entries, as Bazel-generated artifacts
# have a "debug" path that points directly in the Bazel output_base
# and are not known as Ninja artifacts.
api_filter = LastBuildApiFilter.generate_filter(
self._ninja, self._build_dir
)
manifest_json = api_filter.filter_api_json(
"debug_symbols", manifest_json
)
try:
self._debug_parser.parse_manifest_json(manifest_json, module.path)
if self._merge_duplicates:
self._debug_parser.deduplicate_entries()
except ValueError as e:
return _printerr(str(e))
return 0
@property
def debug_symbol_entries(self) -> list[dict[str, T.Any]]:
return self._debug_parser.entries
def cmd_print_debug_symbols(args: argparse.Namespace) -> int:
state = DebugSymbolCommandState(
args.modules,
get_ninja_path(args.fuchsia_dir, args.host_tag),
args.build_dir,
resolve_build_ids=args.resolve_build_ids,
keep_duplicates=args.keep_duplicates,
test_mode=args.test_mode,
)
status = state.parse_manifest(bool(args.last_build_only))
if status != 0:
return status
result = state.debug_symbol_entries
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_export_last_build_debug_symbols(args: argparse.Namespace) -> int:
import debug_symbols
state = DebugSymbolCommandState(
args.modules,
get_ninja_path(args.fuchsia_dir, args.host_tag),
args.build_dir,
resolve_build_ids=True, # Always resolve the .build-id value
keep_duplicates=args.keep_duplicates,
test_mode=args.test_mode,
)
state.parse_manifest(last_build_only=True)
dump_syms_tool = None
gsymutil_tool = None
if args.with_breakpad_symbols:
dump_syms_tool = args.dump_syms
if not dump_syms_tool:
dump_syms_tool = (
args.fuchsia_dir
/ f"prebuilt/third_party/breakpad/{args.host_tag}/dump_syms/dump_syms"
)
if not dump_syms_tool.exists():
if args.host_tag != "linux_x64":
print(
f"WARNING: Ignoring breakpad symbol generation (https://fxbug.dev/447331878).",
file=sys.stderr,
)
dump_syms_tool = None
else:
print(
f"ERROR: Missing breakpad tool, use --dump_syms=TOOL: {dump_syms_tool}",
file=sys.stderr,
)
return 1
if args.with_gsym_symbols:
gsymutil_tool = args.gsymutil
if not gsymutil_tool:
gsymutil_tool = (
args.fuchsia_dir
/ f"prebuilt/third_party/clang/{args.host_tag}/bin/llvm-gsymutil"
)
if not gsymutil_tool.exists():
print(
f"ERROR: Missing gsymutil tool, use --gsymutil=TOOL: {gsymutil_tool}",
file=sys.stderr,
)
return 1
def log_error(error: str) -> None:
print(f"ERROR: {error}", file=sys.stderr)
def log(msg: str) -> None:
if not args.quiet:
print(msg)
exporter = debug_symbols.DebugSymbolExporter(
args.build_dir,
dump_syms_tool=dump_syms_tool,
gsymutil_tool=gsymutil_tool,
log=log,
log_error=log_error,
)
exporter.parse_debug_symbols(state.debug_symbol_entries)
if not exporter.export_debug_symbols(args.output_dir, depth=args.jobs):
return 1
return 0
def cmd_last_ninja_artifacts(args: argparse.Namespace) -> int:
"""Implement the `print_last_ninja_artifacts` command."""
import ninja_artifacts
ninja = get_ninja_path(args.fuchsia_dir, args.host_tag)
ninja_runner = ninja_artifacts.NinjaRunner(ninja)
last_artifacts = ninja_artifacts.get_last_build_artifacts(
args.build_dir, ninja_runner
)
print("\n".join(last_artifacts))
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 cmd_ninja_target_to_gn_labels(args: argparse.Namespace) -> int:
"""Implement the `ninja_target_to_gn_labels` command."""
outputs = OutputsDatabase()
if not outputs.load(args.build_dir):
return 1
ninja_target = args.ninja_target
if not outputs.is_valid_target_name(ninja_target):
print(
f"ERROR: Malformed Ninja target file name: {args.ninja_target}",
file=sys.stderr,
)
return 1
gn_labels = outputs.target_name_to_gn_labels(args.ninja_target)
if gn_labels:
print("\n".join(sorted(gn_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
error_msg = f"Unknown Ninja path: {path}"
if args.allow_targets and outputs.is_valid_target_name(path):
target_labels = outputs.target_name_to_gn_labels(path)
if len(target_labels) == 1:
label_args = qualifier.label_to_build_args(target_labels[0])
_warning(
f"Use '{' '.join(label_args)}' instead of Ninja target '{path}'"
)
return target_labels[0]
if len(target_labels) > 1:
error_msg = (
f"Ambiguous Ninja target name '{path}' matches several GN labels:\n"
+ "\n".join(target_labels)
)
else:
error_msg = f"Unknown Ninja target: {path}"
_error(error_msg)
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: T.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 faster 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.",
)
LastBuildApiFilter.add_parser_arguments(print_parser)
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."
)
LastBuildApiFilter.add_parser_arguments(print_all_parser)
print_all_parser.set_defaults(func=cmd_print_all)
print_debug_symbols_parser = subparsers.add_parser(
"print_debug_symbols",
help="Print flattened debug symbol entries",
description="Print the content of debug_symbols.json and all the files it includes "
"as a single JSON list of entries.",
)
print_debug_symbols_parser.add_argument(
"--pretty", action="store_true", help="Pretty print the JSON output."
)
print_debug_symbols_parser.add_argument(
"--resolve-build-ids",
action="store_true",
help="Force resolution of build-id values.",
)
print_debug_symbols_parser.add_argument(
"--keep-duplicates",
action="store_true",
help="Keep duplicate entries, only used for debugging.",
)
print_debug_symbols_parser.add_argument(
"--test-mode", action="store_true", help="For regression tests only."
)
LastBuildApiFilter.add_parser_arguments(print_debug_symbols_parser)
print_debug_symbols_parser.set_defaults(func=cmd_print_debug_symbols)
last_ninja_artifacts_parser = subparsers.add_parser(
"last_ninja_artifacts",
help="Print the list of Ninja artifacts matching the last build invocation.",
)
last_ninja_artifacts_parser.set_defaults(func=cmd_last_ninja_artifacts)
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 Ninja 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)
ninja_target_to_gn_labels_parser = subparsers.add_parser(
"ninja_target_to_gn_labels",
help="Find all GN labels that output a target with a given file name",
)
ninja_target_to_gn_labels_parser.add_argument(
"ninja_target", help="Ninja target file name only."
)
ninja_target_to_gn_labels_parser.set_defaults(
func=cmd_ninja_target_to_gn_labels
)
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-targets",
action="store_true",
help="Try to convert Ninja target file names (not paths) to GN labels.",
)
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
)
export_last_build_debug_symbols_parser = subparsers.add_parser(
"export_last_build_debug_symbols",
help="Export the debug symbols from last build's artifacts",
description="Export the ELF debug symbol files, and optional breakpad symbol ones, from last build's artifacts.",
)
export_last_build_debug_symbols_parser.add_argument(
"--output-dir",
type=Path,
required=True,
help="Output directory, this will be cleaned before generation.",
)
export_last_build_debug_symbols_parser.add_argument(
"--keep-duplicates",
action="store_true",
help="Keep duplicate entries, only used for debugging this command.",
)
export_last_build_debug_symbols_parser.add_argument(
"--with-breakpad-symbols",
action="store_true",
help="Generate breakpad symbols in the output.",
)
export_last_build_debug_symbols_parser.add_argument(
"--dump_syms",
type=Path,
help="Path to Breakpad dump_syms binary (auto-detected), used by --with-breakpad-symbols.",
)
export_last_build_debug_symbols_parser.add_argument(
"--with-gsym-symbols",
action="store_true",
help="Generate GSYM symbols in the output.",
)
export_last_build_debug_symbols_parser.add_argument(
"--gsymutil",
type=Path,
help="Path to gsymutil tool binary (auto-detected), used by --with-gsym-symbols.",
)
export_last_build_debug_symbols_parser.add_argument(
"-j",
"--jobs",
type=int,
default=0,
help="Specify max concurrent processes to perform symbol generation (default is 0, meaning current core count).",
)
export_last_build_debug_symbols_parser.add_argument(
"--quiet",
action="store_true",
help="Do not print details of operations.",
)
export_last_build_debug_symbols_parser.add_argument(
"--test-mode", action="store_true", help="For regression tests only."
)
export_last_build_debug_symbols_parser.set_defaults(
func=cmd_export_last_build_debug_symbols
)
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:]))