#!/usr/bin/env python3
#
# Copyright 2014 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Writes a .build_config.json file.

This script collects information about a target and all of its transitive
dependencies and writes it to a .build_config.json file for use by other build
steps. It also performs a few validations.

See //build/android/docs/build_config.md for more information.
"""


import argparse
import itertools
import os
import sys
import xml.dom.minidom

from util import build_utils
from util import params_json_util
import action_helpers


class OrderedSet(dict):
  """A simple implementation of an ordered set."""

  def __init__(self, iterable=()):
    super().__init__()
    self.update(iterable)

  def __add__(self, other):
    ret = OrderedSet(self)
    ret.update(other)
    return ret

  def add(self, key):
    self[key] = True

  def remove(self, key):
    self.pop(key, None)

  def update(self, iterable):
    for k in iterable:
      self[k] = True

  def difference_update(self, iterable):
    for k in iterable:
      self.pop(k, None)


class AndroidManifest:
  """Helper class to inspect properties of an AndroidManifest.xml file."""
  def __init__(self, path):
    self.path = path
    dom = xml.dom.minidom.parse(path)
    manifests = dom.getElementsByTagName('manifest')
    assert len(manifests) == 1
    self.manifest = manifests[0]

  def GetInstrumentationElements(self):
    instrumentation_els = self.manifest.getElementsByTagName('instrumentation')
    if len(instrumentation_els) == 0:
      return None
    return instrumentation_els

  def CheckInstrumentationElements(self, expected_package):
    instrs = self.GetInstrumentationElements()
    if not instrs:
      raise Exception('No <instrumentation> elements found in %s' % self.path)
    for instr in instrs:
      instrumented_package = instr.getAttributeNS(
          'http://schemas.android.com/apk/res/android', 'targetPackage')
      if instrumented_package != expected_package:
        raise Exception(
            'Wrong instrumented package. Expected %s, got %s'
            % (expected_package, instrumented_package))

  def GetPackageName(self):
    return self.manifest.getAttribute('package')


class _TransitiveValues:
  """A container for the transitive dependencies of a target.

  This class holds sets of paths for various types of dependencies, such as
  jars, resources, assets, etc.
  """
  # Some values should not be removed when subtracting subtrees.
  _NEVER_REMOVE = frozenset([
      'all_interface_jars',
      'all_input_jars_paths',
      'direct_input_jars_paths',
      'direct_interface_jars',
      'direct_unprocessed_jars',
      'proguard_configs',
  ])

  def __init__(self):
    # Direct classpath:
    self.direct_unprocessed_jars = OrderedSet()
    self.direct_interface_jars = OrderedSet()
    self.direct_input_jars_paths = OrderedSet()
    # Jar files
    self.all_unprocessed_jars = OrderedSet()
    self.all_interface_jars = OrderedSet()
    self.all_input_jars_paths = OrderedSet()
    self.all_dex_files = OrderedSet()
    self.all_processed_jars = OrderedSet()
    # Assets
    self.assets = OrderedSet()
    self.uncompressed_assets = OrderedSet()
    self.locale_paks = OrderedSet()
    # Resources
    self.dependency_zip_overlays = OrderedSet()
    self.dependency_zips = OrderedSet()
    self.extra_package_names = OrderedSet()
    # Other
    self.android_manifests = OrderedSet()
    self.java_resources_jars = OrderedSet()
    self.proguard_configs = OrderedSet()

  def RemoveSubtree(self,
                    other,
                    retain_processed_jars=False,
                    retain_unprocessed_jars=False,
                    retain_resource_zips=False,
                    retain_extra_package_names=False,
                    retain_android_manifests=False):
    """Removes all values from |other| from this instance."""
    for key, value in self.__dict__.items():
      if not (key in _TransitiveValues._NEVER_REMOVE or
              (retain_processed_jars and key == 'all_processed_jars') or
              (retain_unprocessed_jars
               and key in ('all_unprocessed_jars', 'all_interface_jars')) or
              (retain_resource_zips
               and key in ('dependency_zips', 'dependency_zip_overlays')) or
              (retain_extra_package_names and key == 'extra_package_names') or
              (retain_android_manifests and key == 'android_manifests')):
        value.difference_update(getattr(other, key))


def _SortClasspath(dep_list):
  """Sorts a list of dependencies for the classpath.

  Move low priority libraries to the end of the classpath.
  """
  dep_list.sort(key=lambda p: 1 if p.get('low_classpath_priority') else 0)
  return dep_list


class _TransitiveValuesBuilder:

  def __init__(self, params, remove_parent_module_overlap=True):
    self._params = params
    self._remove_parent_module_overlap = remove_parent_module_overlap
    self._ret = _TransitiveValues()

  def Build(self):
    """Computes the transitive dependencies for a given target.

    This is the core logic of the script, collecting all necessary paths and
    metadata from the dependency graph.
    """
    params = self._params

    # System .jar files go into sdk_jars / sdk_interface_jars.
    direct_deps = _SortClasspath(
        params.deps().not_of_type('system_java_library'))
    all_deps = _SortClasspath(
        direct_deps.recursive().not_of_type('system_java_library'))

    all_deps_without_under_test = all_deps
    if apk_under_test_params := params.apk_under_test():
      # Cannot use .append() since this is the cached instance.
      direct_deps = direct_deps + [apk_under_test_params]
      all_deps = _SortClasspath(
          direct_deps.recursive().not_of_type('system_java_library'))

    self._CollectClasspath(direct_deps, all_deps)
    if params.merges_manifests():
      self._CollectManifests(all_deps_without_under_test)
    if params.collects_resources():
      self._CollectResources(all_deps_without_under_test)
    self._CollectExtraPackageNames(direct_deps, all_deps)

    self._ret.proguard_configs.update(params.get('proguard_configs', []))
    self._ret.proguard_configs.update(
        all_deps.collect('proguard_configs', flatten=True))

    if params.is_bundle_module() and self._remove_parent_module_overlap:
      self._RemoveParentModuleOverlap()

    # If there are deps common between dist_jar() and non-dist_jar(), then
    # subtract them off the full classpath to avoid double-deps.
    for dist_jar_params in all_deps.of_type('dist_jar'):
      self._RemoveJarsOwnedByDistJars(dist_jar_params)

    if apk_under_test_params := params.apk_under_test():
      self._RemoveApkUnderTestOverlap(apk_under_test_params)

    if params.is_bundle_module() and not params.is_base_module():
      self._AddBaseModuleToClasspath()
    return self._ret

  def _CollectClasspath(self, direct_deps, all_deps):
    params = self._params
    ret = self._ret
    # Bundle modules do not depend on each other because their names do not
    # match the java naming scheme that identifies them as libraries.
    # TODO(agrieve): Recognize bundle module names as java_library targets so
    #     that they contribute to classpath.
    ret.direct_unprocessed_jars.update(
        direct_deps.collect('unprocessed_jar_path'))
    ret.direct_interface_jars.update(direct_deps.collect('interface_jar_path'))
    ret.all_unprocessed_jars.update(all_deps.collect('unprocessed_jar_path'))
    ret.all_interface_jars.update(all_deps.collect('interface_jar_path'))

    # Deps to add to the compile-time classpath (but not the runtime
    # classpath).
    ret.direct_input_jars_paths.update(params.get('input_jars_paths', []))
    ret.direct_input_jars_paths.update(
        direct_deps.collect('input_jars_paths', flatten=True))
    ret.all_input_jars_paths.update(params.get('input_jars_paths', []))
    ret.all_input_jars_paths.update(
        all_deps.collect('input_jars_paths', flatten=True))

    # Add the target's .jar (except for dist_jar, where it's the output .jar).
    if params.collects_processed_classpath():
      if not params.is_dist_jar():
        if path := params.get('processed_jar_path'):
          ret.all_processed_jars.add(path)
      ret.all_processed_jars.update(all_deps.collect('processed_jar_path'))

    if params.collects_dex_paths():
      if not params.is_dist_jar():
        if path := params.get('dex_path'):
          ret.all_dex_files.add(path)
      ret.all_dex_files.update(all_deps.collect('dex_path'))

  def _AddBaseModuleToClasspath(self):
    base_module = self._params.base_module()
    # Adding base module to classpath to compile against its R.java file
    self._ret.direct_unprocessed_jars.add(base_module['unprocessed_jar_path'])
    self._ret.all_unprocessed_jars.add(base_module['unprocessed_jar_path'])
    self._ret.direct_interface_jars.add(base_module['interface_jar_path'])
    self._ret.all_interface_jars.add(base_module['interface_jar_path'])

  def _CollectManifests(self, all_deps_without_under_test):
    # Manifests are listed from highest priority to lowest priority.
    # Ensure direct manifests come first, then sort the rest by name.
    # https://developer.android.com/build/manage-manifests#merge_priorities
    params = self._params
    ret = self._ret
    ret.android_manifests.update(params.get('mergeable_android_manifests', []))
    indirect_manifests = all_deps_without_under_test.collect(
        'mergeable_android_manifests', flatten=True)
    indirect_manifests.sort(key=lambda p: (os.path.basename(p), p))
    ret.android_manifests.update(indirect_manifests)
    # Prevent the main manifest from showing up in mergeable_android_manifests.
    if path := params.get('android_manifest'):
      if path in ret.android_manifests:
        ret.android_manifests.remove(path)

  def _CollectResources(self, all_deps_without_under_test):
    params = self._params
    ret = self._ret
    resource_deps = params.resource_deps()
    ret.dependency_zips.update(resource_deps.collect('resources_zip'))
    ret.dependency_zip_overlays.update(
        resource_deps.collect('resources_overlay_zip'))

    assets, uncompressed_assets, locale_paks = _MergeAssets(
        all_deps_without_under_test.of_type('android_assets'))
    ret.assets.update(assets)
    ret.uncompressed_assets.update(uncompressed_assets)
    ret.locale_paks = locale_paks

    if params.get('java_resources_jar_path'):
      ret.java_resources_jars.add(params)
    ret.java_resources_jars.update(
        all_deps_without_under_test.collect('java_resources_jar_path'))

  def _CollectExtraPackageNames(self, direct_deps, all_deps):
    # Needed by java_library targets.
    if self._params.is_library():
      # TODO(agrieve): We would ideally move away from being able to use R.java
      # from package_names other than the library's own.
      resource_deps = direct_deps.of_type('android_resources')
    else:
      resource_deps = all_deps.recursive_resource_deps()
    self._ret.extra_package_names.update(resource_deps.collect('package_name'))
    if name := self._params.get('package_name'):
      self._ret.extra_package_names.add(name)

  def _RemoveJarsOwnedByDistJars(self, dist_jar_params):
    """Removes jars from a _TransitiveValues object that are owned by a
    dist_jar.
    """
    if dist_jar_params.get('use_interface_jars'):
      return
    if dist_jar_params.get('direct_deps_only'):
      deps = dist_jar_params.deps()
      dist_jar_tvs = _TransitiveValues()
      dist_jar_tvs.all_unprocessed_jars = deps.collect('unprocessed_jar_path')
      dist_jar_tvs.all_interface_jars = deps.collect('interface_jar_path')
      dist_jar_tvs.all_dex_files = deps.collect('dex_path')
      dist_jar_tvs.all_processed_jars = deps.collect('processed_jar_path')
    else:
      dist_jar_tvs = _TransitiveValuesBuilder(dist_jar_params).Build()

    self._ret.RemoveSubtree(dist_jar_tvs,
                            retain_resource_zips=True,
                            retain_extra_package_names=True)
    self._ret.direct_unprocessed_jars.difference_update(
        dist_jar_tvs.direct_unprocessed_jars)
    self._ret.direct_interface_jars.difference_update(
        dist_jar_tvs.direct_interface_jars)

  def _RemoveParentModuleOverlap(self):
    # There are two approaches to dealing with modules dependencies:
    # 1) Perform steps in android_apk_or_module(), with only the knowledge of
    #    ancesstor splits.
    # 2) Perform steps in android_app_bundle(), with knowledge of full set of
    #    modules. This is required for dex because it can handle the case of
    #    two leaf nodes having the same dep, and promoting that dep to their
    #    common parent.
    #    _PromoteToCommonAncestor() implements this approach.
    #
    # We do 1) unconditionally, and 2) for dex / proguard, but we really
    # should do 2) for all applicable fields.
    if parent_module := self._params.parent_module():
      x = _TransitiveValuesBuilder(parent_module,
                                   remove_parent_module_overlap=False).Build()
      self._ret.RemoveSubtree(x)

  def _RemoveApkUnderTestOverlap(self, apk_under_test_params):
    # The java code for an instrumentation test apk is assembled differently
    # for R8 vs. non-R8.
    #
    # Without R8: Each library's jar is dexed separately and then combined
    # into a single classes.dex. A test apk will include all dex files not
    # present in the apk-under-test. At runtime all test code lives in the
    # test apk, and the program code lives in the apk-under-test.
    #
    # With R8: Each library's .jar file is fed into R8, which outputs
    # a single .jar, which is then dexed into a classes.dex. A test apk
    # includes all jar files from the program and the tests because having
    # them separate doesn't work with R8's whole-program optimizations.
    # Although the apk-under-test still has all of its code in its
    # classes.dex, none of it is used at runtime because the copy within the
    # test apk takes precedence.
    self._ret.RemoveSubtree(
        _TransitiveValuesBuilder(apk_under_test_params).Build(),
        retain_processed_jars=self._params.get('proguard_enabled'),
        retain_unprocessed_jars=True,
        retain_resource_zips=True,
        retain_android_manifests=True)


def _MergeAssets(all_assets):
  """Merges all assets from the given deps.

  Returns:
    A tuple of: (compressed, uncompressed, locale_paks)
    |compressed| and |uncompressed| are lists of "srcPath:zipPath". srcPath is
    the path of the asset to add, and zipPath is the location within the zip
    (excluding assets/ prefix).
    |locale_paks| is a set of all zipPaths that have been marked as
    treat_as_locale_paks=true.
  """
  compressed = {}
  uncompressed = {}
  locale_paks = set()
  for asset_dep in all_assets:
    entry = asset_dep['assets']
    disable_compression = entry.get('disable_compression')
    treat_as_locale_paks = entry.get('treat_as_locale_paks')
    dest_map = uncompressed if disable_compression else compressed
    other_map = compressed if disable_compression else uncompressed
    outputs = entry.get('outputs', [])
    for src, dest in itertools.zip_longest(entry['sources'], outputs):
      if not dest:
        dest = os.path.basename(src)
      # Merge so that each path shows up in only one of the lists, and that
      # deps of the same target override previous ones.
      other_map.pop(dest, 0)
      dest_map[dest] = src
      if treat_as_locale_paks:
        locale_paks.add(dest)

  def create_list(asset_map):
    # Sort to ensure deterministic ordering.
    items = sorted(asset_map.items())
    return [f'{src}:{dest}' for dest, src in items]

  return create_list(compressed), create_list(uncompressed), locale_paks


def _CreateJavaLocaleListFromAssets(assets, locale_paks):
  """Returns a java literal array from a list of locale assets.

  Args:
    assets: A list of all APK asset paths in the form 'src:dst'
    locale_paks: A list of asset paths that correponds to the locale pak
      files of interest. Each |assets| entry will have its 'dst' part matched
      against it to determine if they are part of the result.
  Returns:
    A string that is a Java source literal array listing the locale names
    of the corresponding asset files, without directory or .pak suffix.
    E.g. '{"en-GB", "en-US", "es-ES", "fr", ... }'
  """
  assets_paths = [a.split(':')[1] for a in assets]
  locales = [os.path.basename(a)[:-4] for a in assets_paths if a in locale_paks]
  return '{%s}' % ','.join('"%s"' % l for l in sorted(locales))


def _AddJarMapping(jar_to_target, config):
  """Adds mappings from jar path to GN target for a given config."""
  if jar := config.get('unprocessed_jar_path'):
    jar_to_target[jar] = config['gn_target']
  for jar in config.get('input_jars_paths', []):
    jar_to_target[jar] = config['gn_target']


def _PromoteToCommonAncestor(modules, child_to_ancestors, field_name):
  """Finds duplicates of a field across modules and moves them to the
  nearest common ancestor module. This is used for app bundles to avoid
  duplicating dependencies in multiple modules.
  """
  module_to_fields_set = {}
  for module_name, module in modules.items():
    if field_name in module:
      module_to_fields_set[module_name] = set(module[field_name])

  # Find all items that are duplicated across at least two modules.
  seen = set()
  dupes = set()
  for fields in module_to_fields_set.values():
    new_dupes = seen & fields
    if new_dupes:
      dupes |= new_dupes
    seen |= fields

  for d in dupes:
    owning_modules = []
    for module_name, fields in module_to_fields_set.items():
      if d in fields:
        owning_modules.append(module_name)
    assert len(owning_modules) >= 2
    # Rely on the fact that ancestors are inserted from closest to
    # farthest, where "base" should always be the last element.
    # Arbitrarily using the first owning module - any would work.
    for ancestor in child_to_ancestors[owning_modules[0]]:
      for o in owning_modules[1:]:
        if ancestor not in child_to_ancestors[o] and ancestor is not o:
          break
      else:
        common_ancestor = ancestor
        break
    else:
      raise Exception('Should have already removed ancestor dupes. ' +
                      ','.join(owning_modules) + ' field=' + field_name +
                      ' dupes: ' + ','.join(dupes))
    # Move the duplicated item to the common ancestor.
    for o in owning_modules:
      module_to_fields_set[o].remove(d)
    module_to_fields_set[common_ancestor].add(d)

  # Update the original modules dictionary with the de-duplicated lists.
  for module_name, module in modules.items():
    if field_name in module:
      module[field_name] = sorted(list(module_to_fields_set[module_name]))


def _DoPlatformChecks(params):
  """Check for platform mismatches between a target and its dependencies."""
  # Robolectric is special in that it's an android target that runs on host.
  # You are allowed to depend on both android |deps_require_android| and
  # non-android |deps_not_support_android| targets.
  if params.get('bypass_platform_checks') or params.get('requires_robolectric'):
    return

  deps_require_android = [d for d in params.deps() if d.requires_android()]
  deps_not_support_android = [
      d for d in params.deps() if not d.supports_android()
  ]

  if deps_require_android and not params.requires_android():
    raise Exception('Some deps require building for the Android platform:\n' +
                    '\n'.join('* ' + d['gn_target']
                              for d in deps_require_android))

  if deps_not_support_android and params.supports_android():
    raise Exception('Not all deps support the Android platform:\n' +
                    '\n'.join('* ' + d['gn_target']
                              for d in deps_not_support_android))


def _SuffixAssets(config, target_config):
  """Adds a suffix to asset paths to avoid collisions."""

  def helper(suffix_names, suffix, assets):
    new_assets = []
    for x in assets:
      src_path, apk_subpath = x.split(':', 1)
      if apk_subpath in suffix_names:
        apk_subpath += suffix
      new_assets.append(f'{src_path}:{apk_subpath}')
    return new_assets

  all_assets = target_config['assets'] + target_config['uncompressed_assets']
  suffix = '+' + target_config['package_name'] + '+'
  suffix_names = {x.split(':', 1)[1].replace(suffix, '') for x in all_assets}
  config['assets'] = helper(suffix_names, suffix, config['assets'])
  config['uncompressed_assets'] = helper(suffix_names, suffix,
                                         config['uncompressed_assets'])
  config['apk_assets_suffixed_list'] = ','.join(f'"assets/{x}"'
                                                for x in sorted(suffix_names))
  config['apk_assets_suffix'] = suffix


def _ToTraceEventRewrittenPath(jar_dir, path):
  """Returns the path to the trace-event-rewritten version of a jar."""
  path = path.replace('../', '')
  path = path.replace('obj/', '')
  path = path.replace('gen/', '')
  path = path.replace('.jar', '.tracing_rewritten.jar')
  return os.path.join(jar_dir, path)


def _WriteLintJson(params, lint_json, main_config):
  # Collect all sources and resources at the apk/bundle_module level.
  aars = set()
  srcjars = set()
  sources = set()
  resource_sources = set()
  resource_zips = set()

  if path := params.get('target_sources_file'):
    sources.add(path)
  if paths := params.get('bundled_srcjars'):
    srcjars.update(paths)
  for c in params.deps().recursive():
    if c.get('chromium_code', True) and c.requires_android():
      if path := c.get('target_sources_file'):
        sources.add(path)
      if paths := c.get('bundled_srcjars'):
        srcjars.update(paths)
    if path := c.get('aar_path'):
      aars.add(path)

  for c in params.resource_deps():
    if c.get('chromium_code', True):
      # Prefer res_sources_path to resources_zips so that lint errors have
      # real paths and to avoid needing to extract during lint.
      if path := c.get('res_sources_path'):
        resource_sources.add(path)
      else:
        resource_zips.add(
            c.get('resources_zip') or c.get('resources_overlay_zip'))

  if params.is_bundle():
    classpath = OrderedSet()
    manifests = OrderedSet(p['android_manifest'] for p in params.module_deps())
    for m in params.module_deps():
      module_config = m.build_config_json()
      classpath.update(module_config['javac_full_interface_classpath'])
      manifests.update(module_config['extra_android_manifests'])
    classpath = list(classpath)
    manifests = list(manifests)
  else:
    classpath = main_config['javac_full_interface_classpath']
    manifests = [params['android_manifest']]
    manifests += main_config['extra_android_manifests']

  config = {}
  config['aars'] = sorted(aars)
  config['android_manifests'] = manifests
  config['classpath'] = classpath
  config['sources'] = sorted(sources)
  config['srcjars'] = sorted(srcjars)
  config['resource_sources'] = sorted(resource_sources)
  config['resource_zips'] = sorted(resource_zips)

  build_utils.WriteJson(config, lint_json, only_if_changed=True)


def main():
  parser = argparse.ArgumentParser(
      description='Writes a .build_config.json file.')
  action_helpers.add_depfile_arg(parser)
  parser.add_argument('--output', help='.build_config.json to write.')
  options = parser.parse_args()
  build_config_path = options.output

  params = params_json_util.get_params(
      build_config_path.replace('.build_config.json', '.params.json'))

  if lines := params.get('fail'):
    parser.error('\n'.join(lines))

  target_type = params.type

  is_bundle_module = params.is_bundle_module()
  is_apk = params.is_apk()
  is_apk_or_module = is_apk or is_bundle_module
  is_bundle = params.is_bundle()
  has_classpath = params.has_classpath()
  proguard_enabled = params.get('proguard_enabled', False)

  if has_classpath:
    _DoPlatformChecks(params)

  if is_bundle_module:
    if parent_module_params := params.parent_module():
      # Validate uses_split matches the parent split's name.
      parent_split_name = params.get('uses_split', 'base')
      actual = parent_module_params['module_name']
      assert actual == parent_split_name, (
          f'uses_split={parent_split_name} but parent was {actual}')

  if is_bundle:
    deps_proguard_enabled = []
    deps_proguard_disabled = []
    for module in params.module_deps():
      if module.get('proguard_enabled'):
        deps_proguard_enabled.append(module['module_name'])
      else:
        deps_proguard_disabled.append(module['module_name'])
    if deps_proguard_enabled and deps_proguard_disabled:
      raise Exception('Deps %s have proguard enabled while deps %s have '
                      'proguard disabled' %
                      (deps_proguard_enabled, deps_proguard_disabled))

  apk_under_test_params = params.apk_under_test()

  if is_apk and apk_under_test_params:
    if apk_under_test_params['proguard_enabled']:
      assert proguard_enabled, (
          'proguard must be enabled for '
          'instrumentation apks if it\'s enabled for the tested apk.')

  if is_apk_or_module:
    manifest = AndroidManifest(params['android_manifest'])
    if not apk_under_test_params and manifest.GetInstrumentationElements():
      # This must then have instrumentation only for itself.
      manifest.CheckInstrumentationElements(manifest.GetPackageName())

  config = {}

  if is_apk:
    config['apk_path'] = params['apk_path']
    if path := params.get('incremental_install_json_path'):
      config['incremental_install_json_path'] = path
      config['incremental_apk_path'] = params['incremental_apk_path']

  if is_bundle_module:
    config['unprocessed_jar_path'] = params['unprocessed_jar_path']
    config['res_size_info_path'] = params['res_size_info_path']

  if has_classpath:
    tv = _TransitiveValuesBuilder(params).Build()

    if apk_under_test_params:
      assert is_apk
      config['arsc_package_name'] = (
          apk_under_test_params.build_config_json()['package_name'])

    config['classpath'] = list(tv.direct_unprocessed_jars) + list(
        tv.direct_input_jars_paths)
    config['interface_classpath'] = list(tv.direct_interface_jars) + list(
        tv.direct_input_jars_paths)
    # processor_configs will be of type 'java_annotation_processor', and so not
    # included in deps().recursive().of_type('java_library'). Annotation
    # processors run as part of the build, so need processed_jar_path.
    processor_deps = params.processor_deps()
    config['processor_classpath'] = _SortClasspath(
        processor_deps.recursive()).collect('processed_jar_path')
    config['processor_classes'] = sorted(processor_deps.collect('main_class'))

    config['javac_full_classpath'] = (list(tv.all_unprocessed_jars) +
                                      list(tv.all_input_jars_paths))
    config['javac_full_interface_classpath'] = (list(tv.all_interface_jars) +
                                                list(tv.all_input_jars_paths))

    if params.collects_processed_classpath():
      config['processed_classpath'] = list(tv.all_processed_jars)
      if trace_events_jar_dir := params.get('trace_events_jar_dir'):
        config['trace_event_rewritten_classpath'] = [
            _ToTraceEventRewrittenPath(trace_events_jar_dir, p)
            for p in tv.all_processed_jars
        ]

    if params.collects_dex_paths():
      config['all_dex_files'] = list(tv.all_dex_files)

    if target_type in ('dist_aar', 'java_library'):
      config['dependency_rtxt_files'] = (
          params.resource_deps().collect('rtxt_path'))

    sdk_deps = params.deps().of_type('system_java_library')
    config['sdk_jars'] = sdk_deps.collect('unprocessed_jar_path')
    config['sdk_interface_jars'] = sdk_deps.collect('interface_jar_path')

    if proguard_enabled or target_type == 'dist_aar':
      config['proguard_all_configs'] = sorted(tv.proguard_configs)

    if proguard_enabled:
      config['proguard_classpath_jars'] = sorted(tv.all_input_jars_paths)

    if is_apk_or_module:
      config['java_resources_jars'] = sorted(tv.java_resources_jars)

  if params.is_dist_jar():
    if params.get('direct_deps_only'):
      if params.get('use_interface_jars'):
        dist_jars = tv.direct_interface_jars
      else:
        dist_jars = params.deps().collect('processed_jar_path')
    elif params.get('use_interface_jars'):
      dist_jars = tv.all_interface_jars
    else:
      dist_jars = tv.all_processed_jars

    config['dist_jar'] = {}
    config['dist_jar']['jars'] = list(dist_jars)

  if params.collects_resources():
    # Safe to sort: Build checks that non-overlay resource have no overlap.
    config['dependency_zips'] = sorted(tv.dependency_zips)
    config['dependency_zip_overlays'] = list(tv.dependency_zip_overlays)
    config['assets'] = sorted(tv.assets)
    config['uncompressed_assets'] = sorted(tv.uncompressed_assets)

  if params.get('create_locales_java_list'):
    config['locales_java_list'] = _CreateJavaLocaleListFromAssets(
        tv.uncompressed_assets, tv.locale_paks)

  if params.compiles_resources():
    config['extra_package_names'] = sorted(tv.extra_package_names)

  if params.merges_manifests():
    config['extra_android_manifests'] = list(tv.android_manifests)

  if is_bundle:
    module_deps = params.module_deps()
    module_params_by_name = {m['module_name']: m for m in module_deps}
    module_configs_by_name = {
        m['module_name']: m.build_config_json()
        for m in module_deps
    }
    modules = {m: {} for m in module_params_by_name}

    child_to_ancestors = {n: [] for n in module_params_by_name}
    for name, module in module_params_by_name.items():
      while module := module.parent_module():
        child_to_ancestors[name].append(module['module_name'])

    per_module_fields = [
        'processed_classpath',
        'trace_event_rewritten_classpath',
        'all_dex_files',
        'assets',
        'uncompressed_assets',
    ]
    union_fields = {}
    if params.get('trace_events_jar_dir'):
      union_fields['processed_classpath'] = 'processed_classpath'
      union_fields['trace_event_rewritten_classpath'] = (
          'trace_event_rewritten_classpath')
    if proguard_enabled:
      union_fields['proguard_classpath_jars'] = 'proguard_classpath_jars'
      union_fields['proguard_all_configs'] = 'proguard_all_configs'
      union_fields['sdk_jars'] = 'sdk_jars'

    unioned_values = {k: OrderedSet() for k in union_fields}
    for n, c in module_configs_by_name.items():
      module_params = module_params_by_name[n]
      if n == 'base':
        assert 'base_module_config' not in config, (
            'Must have exactly 1 base module!')
        config['package_name'] = c['package_name']
        config['version_code'] = module_params['version_code']
        config['version_name'] = module_params['version_name']
        config['base_module_config'] = module_params.path
        config['android_manifest'] = module_params['android_manifest']

      for dst_key, src_key in union_fields.items():
        unioned_values[dst_key].update(c[src_key])

      module = modules[n]
      module['final_dex_path'] = module_params['final_dex_path']
      for f in per_module_fields:
        if f in c:
          module[f] = c[f]

    for dst_key in union_fields:
      config[dst_key] = list(unioned_values[dst_key])

    # Promote duplicates from siblings/cousins.
    for f in per_module_fields:
      _PromoteToCommonAncestor(modules, child_to_ancestors, f)
    config['modules'] = modules

  if is_apk_or_module:
    config['package_name'] = manifest.GetPackageName()
    config['android_manifest'] = params['android_manifest']
    config['merged_android_manifest'] = params['merged_android_manifest']

    if is_apk:
      config['version_code'] = params['version_code']
      config['version_name'] = params['version_name']

    # TrichromeLibrary has no dex.
    if final_dex_path := params.get('final_dex_path'):
      config['final_dex_path'] = final_dex_path

    library_paths = params.native_libraries()
    secondary_abi_libraries = params.secondary_abi_native_libraries()
    paths_without_parent_dirs = [
        p for p in secondary_abi_libraries if os.path.sep not in p
    ]
    if paths_without_parent_dirs:
      sys.stderr.write('Found secondary native libraries from primary '
                       'toolchain directory. This is a bug!\n')
      sys.stderr.write('\n'.join(paths_without_parent_dirs))
      sys.stderr.write('\n\nIt may be helpful to run: \n')
      sys.stderr.write('    gn path out/Default //chrome/android:'
                       'monochrome_secondary_abi_lib //base:base\n')
      sys.exit(1)

    config['native'] = {}
    config['native']['libraries'] = library_paths
    config['native']['secondary_abi_libraries'] = secondary_abi_libraries

    if is_bundle_module:
      loadable_modules = params.get('loadable_modules', [])
      loadable_modules.sort()
      secondary_abi_loadable_modules = params.get(
          'secondary_abi_loadable_modules', [])
      secondary_abi_loadable_modules.sort()
      placeholder_paths = params.get('native_lib_placeholders', [])
      placeholder_paths.sort()
      secondary_abi_placeholder_paths = params.get(
          'secondary_native_lib_placeholders', [])
      secondary_abi_placeholder_paths.sort()

      config['native']['loadable_modules'] = loadable_modules
      config['native']['placeholders'] = placeholder_paths
      config['native'][
          'secondary_abi_loadable_modules'] = secondary_abi_loadable_modules
      config['native'][
          'secondary_abi_placeholders'] = secondary_abi_placeholder_paths
      config['native']['library_always_compress'] = params.get(
          'library_always_compress')
      config['proto_resources_path'] = params['proto_resources_path']
      config['base_allowlist_rtxt_path'] = params['base_allowlist_rtxt_path']
      config['rtxt_path'] = params['rtxt_path']
      config['module_pathmap_path'] = params['module_pathmap_path']

  if is_apk_or_module or target_type == 'robolectric_binary':
    if path := params.get('suffix_apk_assets_used_by_config'):
      if path == build_config_path:
        target_config = config
      else:
        target_config = params_json_util.get_build_config(path)
      _SuffixAssets(config, target_config)

  if has_classpath:
    jar_to_target = {}
    all_params = params.deps() + [params]
    if apk_under_test_params:
      all_params.append(apk_under_test_params)
    for d in all_params.recursive():
      _AddJarMapping(jar_to_target, d)

    # Used by check_for_missing_direct_deps.py to give better error message
    # when missing deps are found. Both javac_full_classpath_targets and
    # javac_full_classpath must be in identical orders, as they get passed as
    # separate arrays and then paired up based on index.
    config['javac_full_classpath_targets'] = [
        jar_to_target[x] for x in config['javac_full_classpath']
    ]

  if path := params.get('lint_json'):
    _WriteLintJson(params, path, config)

  build_utils.WriteJson(config, build_config_path, only_if_changed=True)

  if options.depfile:
    all_inputs = params_json_util.all_read_file_paths()
    if path := params.get('shared_libraries_runtime_deps_file'):
      # Path must be added to depfile because the GN template does not add this
      # input to avoid having to depend on the generated_file() target.
      all_inputs.append(path)
    if path := params.get('secondary_abi_shared_libraries_runtime_deps_file'):
      all_inputs.append(path)
    action_helpers.write_depfile(options.depfile, build_config_path, all_inputs)


if __name__ == '__main__':
  main()
