blob: e032e18e10acf388b08a21b663322bf199d514c5 [file] [log] [blame]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provides functions for reading and parsing .params.json files.
This module is used by build scripts to access parameters of GN targets and
their transitive dependencies.
It exposes helper methods to:
* Read and cache JSON files.
* Access transitive dependencies.
* Filter and collect information from dependencies.
The main entry point is `get_params()`, which returns a `ParamsJson` object.
This object is a dictionary-like view of a .params.json file with added helper
methods for dependency traversal.
"""
import functools
import json
import os
# Types that should never be used as a dependency of another build config.
_ROOT_TYPES = frozenset([
'android_apk', 'java_binary', 'java_annotation_processor',
'robolectric_binary', 'android_app_bundle'
])
# Types that should not allow code deps to pass through.
_RESOURCE_TYPES = frozenset(['android_assets', 'android_resources'])
_COMPILE_RESOURCES_TYPES = frozenset([
'android_apk',
'android_app_bundle_module',
'robolectric_binary',
])
_MERGES_MANIFESTS_TYPES = _COMPILE_RESOURCES_TYPES
_COLLECTS_NATIVE_LIBRARIES_TYPES = frozenset([
'android_apk',
'android_app_bundle_module',
'robolectric_binary',
])
_COMPILE_TYPES = frozenset([
'android_apk',
'android_app_bundle_module',
'java_annotation_processor',
'java_binary',
'java_library',
'robolectric_binary',
])
_CLASSPATH_TYPES = frozenset(list(_COMPILE_TYPES) + [
'dist_aar',
'dist_jar',
])
# Track inputs for use in depfiles.
_input_paths = []
# By default scripts are run from the output directory, otherwise call
# set_output_dir() before using methods in this module.
_output_dir_path = ''
def set_output_dir(path):
"""Resolve paths relative to this directory."""
global _output_dir_path
_output_dir_path = path
def all_read_file_paths():
"""Returns a list of all paths read by _get_json()."""
return list(_input_paths)
def _get_json(path):
"""Reads a JSON file and records the path for depfile tracking."""
path = os.path.join(_output_dir_path, path)
_input_paths.append(path)
with open(path, encoding='utf-8') as f:
config = json.load(f)
return config
@functools.cache # pylint: disable=method-cache-max-size-none
def get_build_config(path):
"""Cached version of _get_json() for .build_config.json files."""
return _get_json(path)
@functools.cache # pylint: disable=method-cache-max-size-none
def get_params(path):
"""Returns a cached, dictionary-like object for a .params.json file."""
# It's important to cache the ParamsJson object rather than the json dict
# because some ParamsJson methods use @cache.
return ParamsJson(path, _get_json(path))
def _topological_walk(top, deps_func):
"""Gets the list of all transitive dependencies in topological order.
Args:
top: A list of the top level nodes
deps_func: A function that takes a node and returns a list of its direct
dependencies.
Returns:
A list of all transitive dependencies of nodes in top in order (a node will
appear in the list at a lower index than all of its dependencies).
"""
seen = {}
def discover(nodes):
for node in nodes:
if node in seen:
continue
deps = deps_func(node)
discover(deps)
seen[node] = True
discover(top)
return list(reversed(seen))
def _filter_deps(deps, restrict_to_resource_types=False):
"""Filters dependencies based on their type."""
if restrict_to_resource_types:
# Root types are never is_resource_type().
keep_func = lambda x: x.is_resource_type() or x.is_group()
else:
keep_func = lambda x: not x.is_root_type()
return [d for d in deps if keep_func(d)]
def _collect_public_deps(deps):
"""Recursively collects all public_deps from a list of dependencies."""
ret = []
# public_deps() contains public_deps of public_deps, so no need to recurse.
for x in deps:
ret += x.public_deps()
return ret
def _deps_for_traversal(dep):
"""Returns the direct dependencies of a node for traversal."""
# Do not let restrict_to_resource_types targets traverse groups directly.
# rely on the group's public_deps (which are filtered by
# restrict_to_resource_types) to surface the deps.
return [] if dep.is_group() else dep.deps()
class _HashableList(list):
"""A list that can be used in a set / dict key."""
def __init__(self, *args, sealed=False, **kwargs):
super().__init__(*args, **kwargs)
self._sealed = sealed
def __hash__(self):
self._sealed = True
return id(self)
def __iadd__(self, other):
self._check_not_sealed()
return super().__iadd__(other)
def __add__(self, other):
return DepsList(super().__add__(other))
def _check_not_sealed(self):
assert not self._sealed, 'Cannot mutate a DepsList after it has been used.'
def append(self, value):
self._check_not_sealed()
return super().append(value)
def extend(self, iterable):
self._check_not_sealed()
return super().extend(iterable)
def insert(self, index, value):
self._check_not_sealed()
return super().insert(index, value)
def remove(self, value):
self._check_not_sealed()
return super().remove(value)
def pop(self, index=-1):
self._check_not_sealed()
return super().pop(index)
def clear(self):
self._check_not_sealed()
return super().clear()
def sort(self, *, key=None, reverse=False):
self._check_not_sealed()
return super().sort(key=key, reverse=reverse)
def reverse(self):
self._check_not_sealed()
return super().reverse()
class DepsList(_HashableList):
"""A list of ParamsJson objects with helper methods for traversal."""
def __repr__(self):
return ','.join(repr(x) for x in self)
@functools.cache # pylint: disable=method-cache-max-size-none
def recursive(self):
"""Returns all transitive dependencies."""
# Reverse so that deps appear with higher indices.
return self.walk(lambda x: True)
@functools.cache # pylint: disable=method-cache-max-size-none
def recursive_resource_deps(self):
"""Returns all transitive resource dependencies.
This is a special traversal for resources, which have different rules
for how they depend on other targets.
"""
def helper(x):
if not x.get('recursive_resource_deps'):
return True
# Get unfiltered deps so that libraries come through.
ret = [get_params(p) for p in x.get('public_deps_configs', [])]
ret += _collect_public_deps(ret)
return ret
return self.walk(helper)
def of_type(self, target_type):
"""Filters the list to dependencies of a specific GN target type."""
return DepsList(d for d in self if d.type == target_type)
def not_of_type(self, target_type):
"""Filters the list to dependencies not of a specific GN target type."""
return DepsList(d for d in self if d.type != target_type)
def filter(self, cond):
"""Filters the list based on a custom condition."""
return DepsList(p for p in self if cond(p))
def walk(self, visit_func):
"""Performs a topological walk, allowing for custom traversal logic.
Args:
visit_func: A function called for each node. It can return:
* True (or None): Recurse normally.
* False: Prune this node and its descendants from the result.
* A list: Use this list as the node's children instead of its
actual dependencies.
"""
to_prune = set()
def deps_func(dep):
children = visit_func(dep)
if children in (None, True):
children = _deps_for_traversal(dep)
if children is False:
to_prune.add(dep)
children = []
return children
ret = _topological_walk(self, deps_func)
if to_prune:
ret = [x for x in ret if x not in to_prune]
return DepsList(ret)
def collect(self, key_name, flatten=False):
"""Collects values for a given key from all dependencies in the list.
Args:
key_name: The key to look up in each dependency's params.
flatten: If True, and the values are lists, flatten them into a
single list.
"""
if flatten:
ret = []
for p in self:
if values := p.get(key_name):
ret += values
return ret
return [p[key_name] for p in self if key_name in p]
def _extract_native_libraries_from_runtime_deps(path):
"""Extracts a list of .so paths from a runtime_deps file."""
with open(os.path.join(_output_dir_path, path), encoding='utf-8') as f:
lines = f.read().splitlines()
ret = [
os.path.normpath(l.replace('lib.unstripped/', '')) for l in lines
if l.endswith('.so')
]
ret.reverse()
return ret
class ParamsJson(dict):
"""A dictionary-like view of a .params.json file with helper methods."""
def __init__(self, path, json_dict):
super().__init__(json_dict)
self.path = path
self.type = self['type']
def __hash__(self):
return id(self)
def __eq__(self, other):
return self is other
def __repr__(self):
return f'<{self.path}>'
def build_config_path(self, suffix='.build_config.json'):
"""Returns the .build_config.json path."""
return self.path.replace('.params.json', suffix)
def build_config_json(self):
"""Returns the parsed JSON of the .build_config.json."""
return get_build_config(self.build_config_path())
def javac_build_config_json(self):
"""Returns the parsed JSON of the .javac.build_config.json."""
return get_build_config(self.build_config_path('.javac.build_config.json'))
def manifest_build_config_json(self):
"""Returns the parsed JSON of the .manifest.build_config.json."""
return get_build_config(
self.build_config_path('.manifest.build_config.json'))
def is_root_type(self):
"""Returns True if the target type is a "root" type (e.g., an APK)."""
return self.type in _ROOT_TYPES
def collects_resources(self):
"""Returns True if the target type collects Android resources."""
return self.compiles_resources() or self.type == 'dist_aar'
def compiles_resources(self):
"""Returns True if the target type runs compile_resources.py."""
return self.type in _COMPILE_RESOURCES_TYPES
def merges_manifests(self):
"""Returns True if the target type runs manifest_merger.py."""
return self.type in _MERGES_MANIFESTS_TYPES
def needs_full_javac_classpath(self):
"""Returns True if the target type runs manifest_merger.py."""
return self.type in ('android_apk', 'android_app_bundle_module') or (
self.is_library() and self.get('needs_full_javac_classpath',
False)) or (self.type == 'dist_jar'
and self.requires_android())
def collects_dex_paths(self):
"""Returns True if the target type collects transitive .dex files."""
if self.type in ('dist_aar', 'dist_jar'):
return self.supports_android()
if self.is_bundle_module():
return not self.get('proguard_enabled')
return self.is_apk()
def collects_processed_classpath(self):
"""Returns True if the target type collects the processed classpath."""
if self.get('dex_needs_classpath'):
return True
if self.type in ('dist_aar', 'dist_jar', 'java_binary',
'robolectric_binary'):
return True
if self.is_apk() or self.is_bundle() or self.is_bundle_module():
# Required for is_bundle_module only because write_build_config.py uses
# them as inputs.
return self.get('proguard_enabled', False)
return False
def collects_native_libraries(self):
"""Returns True if the target type collects native libraries."""
return self.type in _COLLECTS_NATIVE_LIBRARIES_TYPES
def has_classpath(self):
"""Returns True if the target type has a classpath."""
if self.is_library():
return bool(self.get('dex_needs_classpath') or not self.is_prebuilt())
return self.type in _CLASSPATH_TYPES
def is_compile_type(self):
"""Returns True if the target has a compile step."""
return not self.is_prebuilt() and self.type in _COMPILE_TYPES
def needs_transitive_rtxt(self):
"""Returns True if the target populates "dependency_rtxt_files"."""
return self.type == 'dist_aar' or (self.is_library()
and not self.is_prebuilt())
def is_prebuilt(self):
"""If it's a java_library prebuilt."""
return self.is_library() and self.get('is_prebuilt', False)
def is_resource_type(self):
"""Returns True if the target is an Android resource type."""
return self.type in _RESOURCE_TYPES
def is_apk(self):
return self.type == 'android_apk'
def is_bundle(self):
return self.type == 'android_app_bundle'
def is_bundle_module(self):
return self.type == 'android_app_bundle_module'
def is_dist_xar(self):
return self.type in ('dist_aar', 'dist_jar')
def is_group(self):
return self.type == 'group'
def is_library(self):
return self.type == 'java_library'
def is_system_library(self):
return self.type == 'system_java_library'
def requires_android(self):
"""Returns True if the target requires the Android platform."""
if self.type.startswith('android') or self.type == 'dist_aar':
return True
return self.is_resource_type or self.get('requires_android', False)
def supports_android(self):
"""Returns True if the target supports the Android platform."""
return self.requires_android() or self.get('supports_android', True)
def _direct_deps(self):
"""Returns only the direct dependencies (from `deps_configs`)."""
# android_resources use only public_dep_configs, so no need for
# restrict_to_resource_types.
return [get_params(p) for p in self.get('deps_configs', [])]
@functools.cache # pylint: disable=method-cache-max-size-none
def _cached_direct_public_deps(self):
"""Returns only the direct public dependencies."""
deps = [get_params(p) for p in self.get('public_deps_configs', [])]
return _filter_deps(deps,
restrict_to_resource_types=self.is_resource_type())
@functools.cache # pylint: disable=method-cache-max-size-none
def deps(self):
"""Returns all dependencies, from both deps and public_deps."""
deps = DepsList(self._direct_deps())
deps += self._cached_direct_public_deps()
deps += _filter_deps(_collect_public_deps(deps),
restrict_to_resource_types=self.is_resource_type())
# Return a cached DepsList so that multiple calls to .deps().recursive()
# result in a cache hit.
return DepsList(dict.fromkeys(deps), sealed=True)
@functools.cache # pylint: disable=method-cache-max-size-none
def public_deps(self):
"""Returns direct public dependencies and their transitive public_deps."""
deps = self._cached_direct_public_deps()
deps = deps + _filter_deps(
_collect_public_deps(deps),
restrict_to_resource_types=self.is_resource_type())
return DepsList(deps, sealed=True)
@functools.cache # pylint: disable=method-cache-max-size-none
def processor_deps(self):
"""Returns all transitive annotation processor dependencies."""
deps = [get_params(p) for p in self.get('processor_configs', [])]
deps += _filter_deps(_collect_public_deps(deps))
return DepsList(dict.fromkeys(deps), sealed=True)
def apk_under_test(self):
"""Returns the ParamsJson for the apk_under_test, or None."""
if path := self.get('apk_under_test_config'):
return get_params(path)
return None
@functools.cache # pylint: disable=method-cache-max-size-none
def module_deps(self):
"""For a bundle, returns the ParamsJson for all module dependencies."""
deps = sorted(self.deps().of_type('android_app_bundle_module'),
key=lambda x: x['module_name'])
if not self.is_bundle():
return deps
base_module = self.base_module()
ret = {base_module: 1}
ret.update(
dict.fromkeys(x for x in deps if x.parent_module() is base_module))
ret.update(dict.fromkeys(deps))
return list(ret)
def parent_module(self):
"""For a bundle module, returns its direct parent module."""
assert self.is_bundle_module(), 'got: ' + self.type
module_deps = self.module_deps()
if self['module_name'] == 'base':
assert not module_deps, ('Base module should not depend on ' +
','.join(module_deps.collect('module_name')))
return None
assert len(module_deps) != 0, 'Should depend on base module'
assert len(module_deps) == 1, (
'Can depend on only one parent module. Found: ' +
','.join(module_deps.collect('module_name')))
return module_deps[0]
def base_module(self):
"""For a bundle module, returns the root 'base' module."""
if self.is_bundle():
return next(x for x in self.deps() if x.get('module_name') == 'base')
assert self.is_bundle_module(), 'got: ' + self.type
# Find the base split.
ret = self
while not ret.is_base_module():
ret = ret.parent_module()
return ret
def is_base_module(self):
"""Returns True if this is the base module of an app bundle."""
return self.is_bundle_module() and self.parent_module() is None
def resource_deps(self):
"""Returns the transitive resource dependencies."""
# For Java libraries, restrict to resource targets that are direct deps, or
# are indirect via other resource targets.
if self.is_library():
return self.deps().of_type('android_resources')
return self.deps().recursive_resource_deps().of_type('android_resources')
def native_libraries(self):
if path := self.get('shared_libraries_runtime_deps_file'):
return _extract_native_libraries_from_runtime_deps(path)
return []
def secondary_abi_native_libraries(self):
if path := self.get('secondary_abi_shared_libraries_runtime_deps_file'):
return _extract_native_libraries_from_runtime_deps(path)
return []