tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

generate_gradle.py (35637B)


      1 #!/usr/bin/env python3
      2 # Copyright 2016 The Chromium Authors
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Generates an Android Studio project from a GN target."""
      7 
      8 import argparse
      9 import codecs
     10 import collections
     11 import glob
     12 import json
     13 import logging
     14 import os
     15 import pathlib
     16 import re
     17 import shlex
     18 import shutil
     19 import subprocess
     20 import sys
     21 
     22 _BUILD_ANDROID = os.path.join(os.path.dirname(__file__), os.pardir)
     23 sys.path.append(_BUILD_ANDROID)
     24 import devil_chromium
     25 from devil.utils import run_tests_helper
     26 from pylib import constants
     27 from pylib.constants import host_paths
     28 
     29 sys.path.append(os.path.join(_BUILD_ANDROID, 'gyp'))
     30 import jinja_template
     31 from util import build_utils
     32 from util import resource_utils
     33 
     34 sys.path.append(os.path.dirname(_BUILD_ANDROID))
     35 import gn_helpers
     36 
     37 # Typically these should track the versions that works on the slowest release
     38 # channel, i.e. Android Studio stable.
     39 _DEFAULT_ANDROID_GRADLE_PLUGIN_VERSION = '7.3.1'
     40 _DEFAULT_KOTLIN_GRADLE_PLUGIN_VERSION = '1.8.0'
     41 _DEFAULT_GRADLE_WRAPPER_VERSION = '7.4'
     42 
     43 _DEPOT_TOOLS_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party',
     44                                 'depot_tools')
     45 _DEFAULT_ANDROID_MANIFEST_PATH = os.path.join(
     46    host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gradle',
     47    'AndroidManifest.xml')
     48 _FILE_DIR = os.path.dirname(__file__)
     49 _GENERATED_JAVA_SUBDIR = 'generated_java'
     50 _JNI_LIBS_SUBDIR = 'symlinked-libs'
     51 _ARMEABI_SUBDIR = 'armeabi'
     52 _GRADLE_BUILD_FILE = 'build.gradle'
     53 _CMAKE_FILE = 'CMakeLists.txt'
     54 # This needs to come first alphabetically among all modules.
     55 _MODULE_ALL = '_all'
     56 _INSTRUMENTATION_TARGET_SUFFIX = '_test_apk__test_apk'
     57 
     58 _DEFAULT_TARGETS = [
     59    '//android_webview/test/embedded_test_server:aw_net_test_support_apk',
     60    '//android_webview/test:webview_instrumentation_apk',
     61    '//android_webview/test:webview_instrumentation_test_apk',
     62    '//base:base_junit_tests',
     63    '//chrome/android:chrome_junit_tests',
     64    '//chrome/android:chrome_public_apk',
     65    '//chrome/android:chrome_public_test_apk',
     66    '//chrome/android:chrome_public_unit_test_apk',
     67    '//chrome/browser/android/examples/inline_autofill_service:inline_autofill_service_example_apk',
     68    '//content/public/android:content_junit_tests',
     69    '//content/shell/android:content_shell_apk',
     70    # Below must be included even with --all since they are libraries.
     71    '//base/android/jni_generator:jni_processor',
     72    '//tools/android/errorprone_plugin:errorprone_plugin_java',
     73 ]
     74 
     75 
     76 def _TemplatePath(name):
     77  return os.path.join(_FILE_DIR, '{}.jinja'.format(name))
     78 
     79 
     80 def _RebasePath(path_or_list, new_cwd=None, old_cwd=None):
     81  """Makes the given path(s) relative to new_cwd, or absolute if not specified.
     82 
     83  If new_cwd is not specified, absolute paths are returned.
     84  If old_cwd is not specified, constants.GetOutDirectory() is assumed.
     85  """
     86  if path_or_list is None:
     87    return []
     88  if not isinstance(path_or_list, str):
     89    return [_RebasePath(p, new_cwd, old_cwd) for p in path_or_list]
     90  if old_cwd is None:
     91    old_cwd = constants.GetOutDirectory()
     92  old_cwd = os.path.abspath(old_cwd)
     93  if new_cwd:
     94    new_cwd = os.path.abspath(new_cwd)
     95    return os.path.relpath(os.path.join(old_cwd, path_or_list), new_cwd)
     96  return os.path.abspath(os.path.join(old_cwd, path_or_list))
     97 
     98 
     99 def _WriteFile(path, data):
    100  """Writes |data| to |path|, constucting parent directories if necessary."""
    101  logging.info('Writing %s', path)
    102  dirname = os.path.dirname(path)
    103  if not os.path.exists(dirname):
    104    os.makedirs(dirname)
    105  with codecs.open(path, 'w', 'utf-8') as output_file:
    106    output_file.write(data)
    107 
    108 
    109 def _RunGnGen(output_dir, args=None):
    110  cmd = [os.path.join(_DEPOT_TOOLS_PATH, 'gn'), 'gen', output_dir]
    111  if args:
    112    cmd.extend(args)
    113  logging.info('Running: %r', cmd)
    114  subprocess.check_call(cmd)
    115 
    116 
    117 def _BuildTargets(output_dir, args):
    118  cmd = gn_helpers.CreateBuildCommand(output_dir)
    119  cmd.extend(args)
    120  logging.info('Running: %s', shlex.join(cmd))
    121  subprocess.check_call(cmd)
    122 
    123 
    124 def _QueryForAllGnTargets(output_dir):
    125  cmd = [
    126      os.path.join(_BUILD_ANDROID, 'list_java_targets.py'), '--gn-labels',
    127      '--nested', '--build', '--output-directory', output_dir
    128  ]
    129  logging.info('Running: %r', cmd)
    130  return subprocess.check_output(cmd, encoding='UTF-8').splitlines()
    131 
    132 
    133 class _ProjectEntry:
    134  """Helper class for project entries."""
    135 
    136  _cached_entries = {}
    137 
    138  def __init__(self, gn_target):
    139    # Use _ProjectEntry.FromGnTarget instead for caching.
    140    self._gn_target = gn_target
    141    self._build_config = None
    142    self._java_files = None
    143    self._all_entries = None
    144    self.android_test_entries = []
    145 
    146  @classmethod
    147  def FromGnTarget(cls, gn_target):
    148    assert gn_target.startswith('//'), gn_target
    149    if ':' not in gn_target:
    150      gn_target = '%s:%s' % (gn_target, os.path.basename(gn_target))
    151    if gn_target not in cls._cached_entries:
    152      cls._cached_entries[gn_target] = cls(gn_target)
    153    return cls._cached_entries[gn_target]
    154 
    155  @classmethod
    156  def FromBuildConfigPath(cls, path):
    157    prefix = 'gen/'
    158    suffix = '.build_config.json'
    159    assert path.startswith(prefix) and path.endswith(suffix), path
    160    subdir = path[len(prefix):-len(suffix)]
    161    gn_target = '//%s:%s' % (os.path.split(subdir))
    162    return cls.FromGnTarget(gn_target)
    163 
    164  def __hash__(self):
    165    return hash(self._gn_target)
    166 
    167  def __eq__(self, other):
    168    return self._gn_target == other.GnTarget()
    169 
    170  def GnTarget(self):
    171    return self._gn_target
    172 
    173  def NinjaTarget(self):
    174    return self._gn_target[2:]
    175 
    176  def BuildConfigPath(self):
    177    return os.path.join('gen', self.GradleSubdir() + '.build_config.json')
    178 
    179  def GradleSubdir(self):
    180    """Returns the output subdirectory."""
    181    ninja_target = self.NinjaTarget()
    182    # Support targets at the root level. e.g. //:foo
    183    if ninja_target[0] == ':':
    184      ninja_target = ninja_target[1:]
    185    return ninja_target.replace(':', os.path.sep)
    186 
    187  def GeneratedJavaSubdir(self):
    188    return _RebasePath(
    189        os.path.join('gen', self.GradleSubdir(), _GENERATED_JAVA_SUBDIR))
    190 
    191  def ProjectName(self):
    192    """Returns the Gradle project name."""
    193    return self.GradleSubdir().replace(os.path.sep, '.')
    194 
    195  def BuildConfig(self):
    196    """Reads and returns the project's .build_config.json JSON."""
    197    if not self._build_config:
    198      with open(_RebasePath(self.BuildConfigPath())) as jsonfile:
    199        self._build_config = json.load(jsonfile)
    200    return self._build_config
    201 
    202  def DepsInfo(self):
    203    return self.BuildConfig()['deps_info']
    204 
    205  def Gradle(self):
    206    return self.BuildConfig()['gradle']
    207 
    208  def Javac(self):
    209    return self.BuildConfig()['javac']
    210 
    211  def GetType(self):
    212    """Returns the target type from its .build_config."""
    213    return self.DepsInfo()['type']
    214 
    215  def IsValid(self):
    216    return self.GetType() in (
    217        'android_apk',
    218        'android_app_bundle_module',
    219        'java_library',
    220        "java_annotation_processor",
    221        'java_binary',
    222        'robolectric_binary',
    223    )
    224 
    225  def ResSources(self):
    226    return self.DepsInfo().get('lint_resource_sources', [])
    227 
    228  def JavaFiles(self):
    229    if self._java_files is None:
    230      target_sources_file = self.DepsInfo().get('target_sources_file')
    231      java_files = []
    232      if target_sources_file:
    233        target_sources_file = _RebasePath(target_sources_file)
    234        java_files = build_utils.ReadSourcesList(target_sources_file)
    235      self._java_files = java_files
    236    return self._java_files
    237 
    238  def PrebuiltJars(self):
    239    return self.Gradle().get('dependent_prebuilt_jars', [])
    240 
    241  def AllEntries(self):
    242    """Returns a list of all entries that the current entry depends on.
    243 
    244    This includes the entry itself to make iterating simpler."""
    245    if self._all_entries is None:
    246      logging.debug('Generating entries for %s', self.GnTarget())
    247      deps = [_ProjectEntry.FromBuildConfigPath(p)
    248          for p in self.Gradle()['dependent_android_projects']]
    249      deps.extend(_ProjectEntry.FromBuildConfigPath(p)
    250          for p in self.Gradle()['dependent_java_projects'])
    251      all_entries = set()
    252      for dep in deps:
    253        all_entries.update(dep.AllEntries())
    254      all_entries.add(self)
    255      self._all_entries = list(all_entries)
    256    return self._all_entries
    257 
    258 
    259 class _ProjectContextGenerator:
    260  """Helper class to generate gradle build files"""
    261  def __init__(self, project_dir, build_vars, use_gradle_process_resources,
    262               jinja_processor, split_projects):
    263    self.project_dir = project_dir
    264    self.build_vars = build_vars
    265    self.use_gradle_process_resources = use_gradle_process_resources
    266    self.jinja_processor = jinja_processor
    267    self.split_projects = split_projects
    268    self.processed_java_dirs = set()
    269    self.processed_prebuilts = set()
    270    self.processed_res_dirs = set()
    271 
    272  def _GenJniLibs(self, root_entry):
    273    libraries = []
    274    for entry in self._GetEntries(root_entry):
    275      libraries += entry.BuildConfig().get('native', {}).get('libraries', [])
    276    if libraries:
    277      return _CreateJniLibsDir(constants.GetOutDirectory(),
    278          self.EntryOutputDir(root_entry), libraries)
    279    return []
    280 
    281  def _GenJavaDirs(self, root_entry):
    282    java_files = []
    283    for entry in self._GetEntries(root_entry):
    284      java_files += entry.JavaFiles()
    285    java_dirs, excludes = _ComputeJavaSourceDirsAndExcludes(
    286        constants.GetOutDirectory(), java_files)
    287    return java_dirs, excludes
    288 
    289  def _GenCustomManifest(self, entry):
    290    """Returns the path to the generated AndroidManifest.xml.
    291 
    292    Gradle uses package id from manifest when generating R.class. So, we need
    293    to generate a custom manifest if we let gradle process resources. We cannot
    294    simply set android.defaultConfig.applicationId because it is not supported
    295    for library targets."""
    296    resource_packages = entry.Javac().get('resource_packages')
    297    if not resource_packages:
    298      logging.debug(
    299          'Target %s includes resources from unknown package. '
    300          'Unable to process with gradle.', entry.GnTarget())
    301      return _DEFAULT_ANDROID_MANIFEST_PATH
    302    if len(resource_packages) > 1:
    303      logging.debug(
    304          'Target %s includes resources from multiple packages. '
    305          'Unable to process with gradle.', entry.GnTarget())
    306      return _DEFAULT_ANDROID_MANIFEST_PATH
    307 
    308    variables = {'package': resource_packages[0]}
    309    data = self.jinja_processor.Render(_TemplatePath('manifest'), variables)
    310    output_file = os.path.join(
    311        self.EntryOutputDir(entry), 'AndroidManifest.xml')
    312    _WriteFile(output_file, data)
    313 
    314    return output_file
    315 
    316  def _Relativize(self, entry, paths):
    317    return _RebasePath(paths, self.EntryOutputDir(entry))
    318 
    319  def _GetEntries(self, entry):
    320    if self.split_projects:
    321      return [entry]
    322    return entry.AllEntries()
    323 
    324  def EntryOutputDir(self, entry):
    325    return os.path.join(self.project_dir, entry.GradleSubdir())
    326 
    327  def GeneratedInputs(self, root_entry):
    328    generated_inputs = set()
    329    for entry in self._GetEntries(root_entry):
    330      generated_inputs.update(entry.PrebuiltJars())
    331    return generated_inputs
    332 
    333  def GenerateManifest(self, root_entry):
    334    android_manifest = root_entry.DepsInfo().get('android_manifest')
    335    if not android_manifest:
    336      android_manifest = self._GenCustomManifest(root_entry)
    337    return self._Relativize(root_entry, android_manifest)
    338 
    339  def Generate(self, root_entry):
    340    # TODO(agrieve): Add an option to use interface jars and see if that speeds
    341    # things up at all.
    342    variables = {}
    343    java_dirs, excludes = self._GenJavaDirs(root_entry)
    344    java_dirs.extend(
    345        e.GeneratedJavaSubdir() for e in self._GetEntries(root_entry))
    346    self.processed_java_dirs.update(java_dirs)
    347    java_dirs.sort()
    348    variables['java_dirs'] = self._Relativize(root_entry, java_dirs)
    349    variables['java_excludes'] = excludes
    350    variables['jni_libs'] = self._Relativize(
    351        root_entry, set(self._GenJniLibs(root_entry)))
    352    prebuilts = set(
    353        p for e in self._GetEntries(root_entry) for p in e.PrebuiltJars())
    354    self.processed_prebuilts.update(prebuilts)
    355    variables['prebuilts'] = self._Relativize(root_entry, prebuilts)
    356    res_sources_files = _RebasePath(
    357        set(p for e in self._GetEntries(root_entry) for p in e.ResSources()))
    358    res_sources = []
    359    for res_sources_file in res_sources_files:
    360      res_sources.extend(build_utils.ReadSourcesList(res_sources_file))
    361    res_dirs = resource_utils.DeduceResourceDirsFromFileList(res_sources)
    362    # Do not add generated resources for the all module since it creates many
    363    # duplicates, and currently resources are only used for editing.
    364    self.processed_res_dirs.update(res_dirs)
    365    variables['res_dirs'] = self._Relativize(root_entry, res_dirs)
    366    if self.split_projects:
    367      deps = [_ProjectEntry.FromBuildConfigPath(p)
    368              for p in root_entry.Gradle()['dependent_android_projects']]
    369      variables['android_project_deps'] = [d.ProjectName() for d in deps]
    370      deps = [_ProjectEntry.FromBuildConfigPath(p)
    371              for p in root_entry.Gradle()['dependent_java_projects']]
    372      variables['java_project_deps'] = [d.ProjectName() for d in deps]
    373    return variables
    374 
    375 
    376 def _ComputeJavaSourceDirs(java_files):
    377  """Returns a dictionary of source dirs with each given files in one."""
    378  found_roots = {}
    379  for path in java_files:
    380    path_root = path
    381    # Recognize these tokens as top-level.
    382    while True:
    383      path_root = os.path.dirname(path_root)
    384      basename = os.path.basename(path_root)
    385      assert basename, 'Failed to find source dir for ' + path
    386      if basename in ('java', 'src'):
    387        break
    388      if basename in ('javax', 'org', 'com'):
    389        path_root = os.path.dirname(path_root)
    390        break
    391    if path_root not in found_roots:
    392      found_roots[path_root] = []
    393    found_roots[path_root].append(path)
    394  return found_roots
    395 
    396 
    397 def _ComputeExcludeFilters(wanted_files, unwanted_files, parent_dir):
    398  """Returns exclude patters to exclude unwanted files but keep wanted files.
    399 
    400  - Shortens exclude list by globbing if possible.
    401  - Exclude patterns are relative paths from the parent directory.
    402  """
    403  excludes = []
    404  files_to_include = set(wanted_files)
    405  files_to_exclude = set(unwanted_files)
    406  while files_to_exclude:
    407    unwanted_file = files_to_exclude.pop()
    408    target_exclude = os.path.join(
    409        os.path.dirname(unwanted_file), '*.java')
    410    found_files = set(glob.glob(target_exclude))
    411    valid_files = found_files & files_to_include
    412    if valid_files:
    413      excludes.append(os.path.relpath(unwanted_file, parent_dir))
    414    else:
    415      excludes.append(os.path.relpath(target_exclude, parent_dir))
    416      files_to_exclude -= found_files
    417  return excludes
    418 
    419 
    420 def _ComputeJavaSourceDirsAndExcludes(output_dir, source_files):
    421  """Computes the list of java source directories and exclude patterns.
    422 
    423  This includes both Java and Kotlin files since both are listed in the same
    424  "java" section for gradle.
    425 
    426  1. Computes the root source directories from the list of files.
    427  2. Compute exclude patterns that exclude all extra files only.
    428  3. Returns the list of source directories and exclude patterns.
    429  """
    430  java_dirs = []
    431  excludes = []
    432  if source_files:
    433    source_files = _RebasePath(source_files)
    434    computed_dirs = _ComputeJavaSourceDirs(source_files)
    435    java_dirs = list(computed_dirs.keys())
    436    all_found_source_files = set()
    437 
    438    for directory, files in computed_dirs.items():
    439      found_source_files = (build_utils.FindInDirectory(directory, '*.java') +
    440                            build_utils.FindInDirectory(directory, '*.kt'))
    441      all_found_source_files.update(found_source_files)
    442      unwanted_source_files = set(found_source_files) - set(files)
    443      if unwanted_source_files:
    444        logging.debug('Directory requires excludes: %s', directory)
    445        excludes.extend(
    446            _ComputeExcludeFilters(files, unwanted_source_files, directory))
    447 
    448    missing_source_files = set(source_files) - all_found_source_files
    449    # Warn only about non-generated files that are missing.
    450    missing_source_files = [
    451        p for p in missing_source_files if not p.startswith(output_dir)
    452    ]
    453    if missing_source_files:
    454      logging.warning('Some source files were not found: %s',
    455                      missing_source_files)
    456 
    457  return java_dirs, excludes
    458 
    459 
    460 def _CreateRelativeSymlink(target_path, link_path):
    461  link_dir = os.path.dirname(link_path)
    462  relpath = os.path.relpath(target_path, link_dir)
    463  logging.debug('Creating symlink %s -> %s', link_path, relpath)
    464  if not os.path.exists(link_dir):
    465    os.makedirs(link_dir)
    466  os.symlink(relpath, link_path)
    467 
    468 
    469 def _CreateJniLibsDir(output_dir, entry_output_dir, so_files):
    470  """Creates directory with symlinked .so files if necessary.
    471 
    472  Returns list of JNI libs directories."""
    473 
    474  if so_files:
    475    symlink_dir = os.path.join(entry_output_dir, _JNI_LIBS_SUBDIR)
    476    shutil.rmtree(symlink_dir, True)
    477    abi_dir = os.path.join(symlink_dir, _ARMEABI_SUBDIR)
    478    for so_file in so_files:
    479      target_path = os.path.join(output_dir, so_file)
    480      symlinked_path = os.path.join(abi_dir, so_file)
    481      _CreateRelativeSymlink(target_path, symlinked_path)
    482 
    483    return [symlink_dir]
    484 
    485  return []
    486 
    487 
    488 def _ParseVersionFromFile(file_path, version_regex_string, default_version):
    489  if os.path.exists(file_path):
    490    content = pathlib.Path(file_path).read_text()
    491    match = re.search(version_regex_string, content)
    492    if match:
    493      version = match.group(1)
    494      logging.info('Using existing version %s in %s.', version, file_path)
    495      return version
    496    logging.warning('Unable to find %s in %s:\n%s', version_regex_string,
    497                    file_path, content)
    498  return default_version
    499 
    500 
    501 def _GenerateLocalProperties(sdk_dir):
    502  """Returns the data for local.properties as a string."""
    503  return '\n'.join([
    504      '# Generated by //build/android/gradle/generate_gradle.py',
    505      'sdk.dir=%s' % sdk_dir,
    506      '',
    507  ])
    508 
    509 
    510 def _GenerateGradleWrapperProperties(file_path):
    511  """Returns the data for gradle-wrapper.properties as a string."""
    512 
    513  version = _ParseVersionFromFile(file_path,
    514                                  r'/distributions/gradle-([\d.]+)-all.zip',
    515                                  _DEFAULT_GRADLE_WRAPPER_VERSION)
    516 
    517  return '\n'.join([
    518      '# Generated by //build/android/gradle/generate_gradle.py',
    519      ('distributionUrl=https\\://services.gradle.org'
    520       f'/distributions/gradle-{version}-all.zip'),
    521      '',
    522  ])
    523 
    524 
    525 def _GenerateGradleProperties():
    526  """Returns the data for gradle.properties as a string."""
    527  return '\n'.join([
    528      '# Generated by //build/android/gradle/generate_gradle.py',
    529      '',
    530      '# Tells Gradle to show warnings during project sync.',
    531      'org.gradle.warning.mode=all',
    532      '',
    533  ])
    534 
    535 
    536 def _GenerateBaseVars(generator, build_vars):
    537  variables = {}
    538  # Avoid pre-release SDKs since Studio might not know how to download them.
    539  variables['compile_sdk_version'] = (
    540      'android-%s' % build_vars['android_sdk_platform_version'])
    541  target_sdk_version = build_vars['android_sdk_platform_version']
    542  if str(target_sdk_version).isalpha():
    543    target_sdk_version = '"{}"'.format(target_sdk_version)
    544  variables['target_sdk_version'] = target_sdk_version
    545  variables['min_sdk_version'] = build_vars['default_min_sdk_version']
    546  variables['use_gradle_process_resources'] = (
    547      generator.use_gradle_process_resources)
    548  return variables
    549 
    550 
    551 def _GenerateGradleFile(entry, generator, build_vars, jinja_processor):
    552  """Returns the data for a project's build.gradle."""
    553  deps_info = entry.DepsInfo()
    554  variables = _GenerateBaseVars(generator, build_vars)
    555  sourceSetName = 'main'
    556 
    557  if deps_info['type'] == 'android_apk':
    558    target_type = 'android_apk'
    559  elif deps_info['type'] in ('java_library', 'java_annotation_processor'):
    560    is_prebuilt = deps_info.get('is_prebuilt', False)
    561    gradle_treat_as_prebuilt = deps_info.get('gradle_treat_as_prebuilt', False)
    562    if is_prebuilt or gradle_treat_as_prebuilt:
    563      return None
    564    if deps_info['requires_android']:
    565      target_type = 'android_library'
    566    else:
    567      target_type = 'java_library'
    568  elif deps_info['type'] == 'java_binary':
    569    target_type = 'java_binary'
    570    variables['main_class'] = deps_info.get('main_class')
    571  elif deps_info['type'] == 'robolectric_binary':
    572    target_type = 'android_junit'
    573    sourceSetName = 'test'
    574  else:
    575    return None
    576 
    577  variables['target_name'] = os.path.splitext(deps_info['name'])[0]
    578  variables['template_type'] = target_type
    579  variables['main'] = {}
    580  variables[sourceSetName] = generator.Generate(entry)
    581  variables['main']['android_manifest'] = generator.GenerateManifest(entry)
    582 
    583  if entry.android_test_entries:
    584    variables['android_test'] = []
    585    for e in entry.android_test_entries:
    586      test_entry = generator.Generate(e)
    587      test_entry['android_manifest'] = generator.GenerateManifest(e)
    588      variables['android_test'].append(test_entry)
    589      for key, value in test_entry.items():
    590        if isinstance(value, list):
    591          test_entry[key] = sorted(set(value) - set(variables['main'][key]))
    592 
    593  return jinja_processor.Render(
    594      _TemplatePath(target_type.split('_')[0]), variables)
    595 
    596 
    597 # Example: //chrome/android:monochrome
    598 def _GetNative(relative_func, target_names):
    599  """Returns an object containing native c++ sources list and its included path
    600 
    601  Iterate through all target_names and their deps to get the list of included
    602  paths and sources."""
    603  out_dir = constants.GetOutDirectory()
    604  with open(os.path.join(out_dir, 'project.json'), 'r') as project_file:
    605    projects = json.load(project_file)
    606  project_targets = projects['targets']
    607  root_dir = projects['build_settings']['root_path']
    608  includes = set()
    609  processed_target = set()
    610  targets_stack = list(target_names)
    611  sources = []
    612 
    613  while targets_stack:
    614    target_name = targets_stack.pop()
    615    if target_name in processed_target:
    616      continue
    617    processed_target.add(target_name)
    618    target = project_targets[target_name]
    619    includes.update(target.get('include_dirs', []))
    620    targets_stack.extend(target.get('deps', []))
    621    # Ignore generated files
    622    sources.extend(f for f in target.get('sources', [])
    623                   if f.endswith('.cc') and not f.startswith('//out'))
    624 
    625  def process_paths(paths):
    626    # Ignores leading //
    627    return relative_func(
    628        sorted(os.path.join(root_dir, path[2:]) for path in paths))
    629 
    630  return {
    631      'sources': process_paths(sources),
    632      'includes': process_paths(includes),
    633  }
    634 
    635 
    636 def _GenerateModuleAll(gradle_output_dir, generator, build_vars,
    637                       jinja_processor, native_targets):
    638  """Returns the data for a pseudo build.gradle of all dirs.
    639 
    640  See //docs/android_studio.md for more details."""
    641  variables = _GenerateBaseVars(generator, build_vars)
    642  target_type = 'android_apk'
    643  variables['target_name'] = _MODULE_ALL
    644  variables['template_type'] = target_type
    645  java_dirs = sorted(generator.processed_java_dirs)
    646  prebuilts = sorted(generator.processed_prebuilts)
    647  res_dirs = sorted(generator.processed_res_dirs)
    648  def Relativize(paths):
    649    return _RebasePath(paths, os.path.join(gradle_output_dir, _MODULE_ALL))
    650 
    651  # As after clank modularization, the java and javatests code will live side by
    652  # side in the same module, we will list both of them in the main target here.
    653  main_java_dirs = [d for d in java_dirs if 'junit/' not in d]
    654  junit_test_java_dirs = [d for d in java_dirs if 'junit/' in d]
    655  variables['main'] = {
    656      'android_manifest': Relativize(_DEFAULT_ANDROID_MANIFEST_PATH),
    657      'java_dirs': Relativize(main_java_dirs),
    658      'prebuilts': Relativize(prebuilts),
    659      'java_excludes': ['**/*.java', '**/*.kt'],
    660      'res_dirs': Relativize(res_dirs),
    661  }
    662  variables['android_test'] = [{
    663      'java_dirs': Relativize(junit_test_java_dirs),
    664      'java_excludes': ['**/*.java', '**/*.kt'],
    665  }]
    666  if native_targets:
    667    variables['native'] = _GetNative(
    668        relative_func=Relativize, target_names=native_targets)
    669  data = jinja_processor.Render(
    670      _TemplatePath(target_type.split('_')[0]), variables)
    671  _WriteFile(
    672      os.path.join(gradle_output_dir, _MODULE_ALL, _GRADLE_BUILD_FILE), data)
    673  if native_targets:
    674    cmake_data = jinja_processor.Render(_TemplatePath('cmake'), variables)
    675    _WriteFile(
    676        os.path.join(gradle_output_dir, _MODULE_ALL, _CMAKE_FILE), cmake_data)
    677 
    678 
    679 def _GenerateRootGradle(jinja_processor, file_path):
    680  """Returns the data for the root project's build.gradle."""
    681  android_gradle_plugin_version = _ParseVersionFromFile(
    682      file_path, r'com.android.tools.build:gradle:([\d.]+)',
    683      _DEFAULT_ANDROID_GRADLE_PLUGIN_VERSION)
    684  kotlin_gradle_plugin_version = _ParseVersionFromFile(
    685      file_path, r'org.jetbrains.kotlin:kotlin-gradle-plugin:([\d.]+)',
    686      _DEFAULT_KOTLIN_GRADLE_PLUGIN_VERSION)
    687 
    688  return jinja_processor.Render(
    689      _TemplatePath('root'), {
    690          'android_gradle_plugin_version': android_gradle_plugin_version,
    691          'kotlin_gradle_plugin_version': kotlin_gradle_plugin_version,
    692      })
    693 
    694 
    695 def _GenerateSettingsGradle(project_entries):
    696  """Returns the data for settings.gradle."""
    697  project_name = os.path.basename(os.path.dirname(host_paths.DIR_SOURCE_ROOT))
    698  lines = []
    699  lines.append('// Generated by //build/android/gradle/generate_gradle.py')
    700  lines.append('rootProject.name = "%s"' % project_name)
    701  lines.append('rootProject.projectDir = settingsDir')
    702  lines.append('')
    703  for name, subdir in project_entries:
    704    # Example target:
    705    # android_webview:android_webview_java__build_config_crbug_908819
    706    lines.append('include ":%s"' % name)
    707    lines.append('project(":%s").projectDir = new File(settingsDir, "%s")' %
    708                 (name, subdir))
    709  return '\n'.join(lines)
    710 
    711 
    712 def _FindAllProjectEntries(main_entries):
    713  """Returns the list of all _ProjectEntry instances given the root project."""
    714  found = set()
    715  to_scan = list(main_entries)
    716  while to_scan:
    717    cur_entry = to_scan.pop()
    718    if cur_entry in found:
    719      continue
    720    found.add(cur_entry)
    721    sub_config_paths = cur_entry.DepsInfo()['deps_configs']
    722    to_scan.extend(
    723        _ProjectEntry.FromBuildConfigPath(p) for p in sub_config_paths)
    724  return list(found)
    725 
    726 
    727 def _CombineTestEntries(entries):
    728  """Combines test apks into the androidTest source set of their target.
    729 
    730  - Speeds up android studio
    731  - Adds proper dependency between test and apk_under_test
    732  - Doesn't work for junit yet due to resulting circular dependencies
    733    - e.g. base_junit_tests > base_junit_test_support > base_java
    734  """
    735  combined_entries = []
    736  android_test_entries = collections.defaultdict(list)
    737  for entry in entries:
    738    target_name = entry.GnTarget()
    739    if (target_name.endswith(_INSTRUMENTATION_TARGET_SUFFIX)
    740        and 'apk_under_test' in entry.Gradle()):
    741      apk_name = entry.Gradle()['apk_under_test']
    742      android_test_entries[apk_name].append(entry)
    743    else:
    744      combined_entries.append(entry)
    745  for entry in combined_entries:
    746    target_name = entry.DepsInfo()['name']
    747    if target_name in android_test_entries:
    748      entry.android_test_entries = android_test_entries[target_name]
    749      del android_test_entries[target_name]
    750  # Add unmatched test entries as individual targets.
    751  combined_entries.extend(e for l in android_test_entries.values() for e in l)
    752  return combined_entries
    753 
    754 
    755 def main():
    756  parser = argparse.ArgumentParser()
    757  parser.add_argument('--output-directory',
    758                      help='Path to the root build directory.')
    759  parser.add_argument('-v',
    760                      '--verbose',
    761                      dest='verbose_count',
    762                      default=0,
    763                      action='count',
    764                      help='Verbose level')
    765  parser.add_argument('--target',
    766                      dest='targets',
    767                      action='append',
    768                      help='GN target to generate project for. Replaces set of '
    769                           'default targets. May be repeated.')
    770  parser.add_argument('--extra-target',
    771                      dest='extra_targets',
    772                      action='append',
    773                      help='GN target to generate project for, in addition to '
    774                           'the default ones. May be repeated.')
    775  parser.add_argument('--project-dir',
    776                      help='Root of the output project.',
    777                      default=os.path.join('$CHROMIUM_OUTPUT_DIR', 'gradle'))
    778  parser.add_argument('--all',
    779                      action='store_true',
    780                      help='Include all .java files reachable from any '
    781                           'apk/test/binary target. On by default unless '
    782                           '--split-projects is used (--split-projects can '
    783                           'slow down Studio given too many targets).')
    784  parser.add_argument('--use-gradle-process-resources',
    785                      action='store_true',
    786                      help='Have gradle generate R.java rather than ninja')
    787  parser.add_argument('--split-projects',
    788                      action='store_true',
    789                      help='Split projects by their gn deps rather than '
    790                           'combining all the dependencies of each target')
    791  parser.add_argument('--native-target',
    792                      dest='native_targets',
    793                      action='append',
    794                      help='GN native targets to generate for. May be '
    795                           'repeated.')
    796  parser.add_argument(
    797      '--sdk-path',
    798      default=os.path.expanduser('~/Android/Sdk'),
    799      help='The path to use as the SDK root, overrides the '
    800      'default at ~/Android/Sdk.')
    801  args = parser.parse_args()
    802  if args.output_directory:
    803    constants.SetOutputDirectory(args.output_directory)
    804  constants.CheckOutputDirectory()
    805  output_dir = constants.GetOutDirectory()
    806  devil_chromium.Initialize(output_directory=output_dir)
    807  run_tests_helper.SetLogLevel(args.verbose_count)
    808 
    809  if args.use_gradle_process_resources:
    810    assert args.split_projects, (
    811        'Gradle resources does not work without --split-projects.')
    812 
    813  _gradle_output_dir = os.path.abspath(
    814      args.project_dir.replace('$CHROMIUM_OUTPUT_DIR', output_dir))
    815  logging.warning('Creating project at: %s', _gradle_output_dir)
    816 
    817  # Generate for "all targets" by default when not using --split-projects (too
    818  # slow), and when no --target has been explicitly set. "all targets" means all
    819  # java targets that are depended on by an apk or java_binary (leaf
    820  # java_library targets will not be included).
    821  args.all = args.all or (not args.split_projects and not args.targets)
    822 
    823  targets_from_args = set(args.targets or _DEFAULT_TARGETS)
    824  if args.extra_targets:
    825    targets_from_args.update(args.extra_targets)
    826 
    827  if args.all:
    828    if args.native_targets:
    829      _RunGnGen(output_dir, ['--ide=json'])
    830    elif not os.path.exists(os.path.join(output_dir, 'build.ninja')):
    831      _RunGnGen(output_dir)
    832    else:
    833      # Faster than running "gn gen" in the no-op case.
    834      _BuildTargets(output_dir, ['build.ninja'])
    835    # Query ninja for all __build_config_crbug_908819 targets.
    836    targets = _QueryForAllGnTargets(output_dir)
    837  else:
    838    assert not args.native_targets, 'Native editing requires --all.'
    839    targets = [
    840        re.sub(r'_test_apk$', _INSTRUMENTATION_TARGET_SUFFIX, t)
    841        for t in targets_from_args
    842    ]
    843    # Necessary after "gn clean"
    844    if not os.path.exists(
    845        os.path.join(output_dir, gn_helpers.BUILD_VARS_FILENAME)):
    846      _RunGnGen(output_dir)
    847 
    848  main_entries = [_ProjectEntry.FromGnTarget(t) for t in targets]
    849  if not args.all:
    850    # list_java_targets.py takes care of building .build_config.json in the
    851    # --all case.
    852    _BuildTargets(output_dir, [t.BuildConfigPath() for t in main_entries])
    853 
    854  build_vars = gn_helpers.ReadBuildVars(output_dir)
    855  jinja_processor = jinja_template.JinjaProcessor(_FILE_DIR)
    856  generator = _ProjectContextGenerator(_gradle_output_dir, build_vars,
    857                                       args.use_gradle_process_resources,
    858                                       jinja_processor, args.split_projects)
    859 
    860  if args.all:
    861    # There are many unused libraries, so restrict to those that are actually
    862    # used by apks/bundles/binaries/tests or that are explicitly mentioned in
    863    # --targets.
    864    BASE_TYPES = ('android_apk', 'android_app_bundle_module', 'java_binary',
    865                  'robolectric_binary')
    866    main_entries = [
    867        e for e in main_entries
    868        if (e.GetType() in BASE_TYPES or e.GnTarget() in targets_from_args
    869            or e.GnTarget().endswith(_INSTRUMENTATION_TARGET_SUFFIX))
    870    ]
    871 
    872  if args.split_projects:
    873    main_entries = _FindAllProjectEntries(main_entries)
    874 
    875  logging.info('Generating for %d targets.', len(main_entries))
    876 
    877  entries = [e for e in _CombineTestEntries(main_entries) if e.IsValid()]
    878  logging.info('Creating %d projects for targets.', len(entries))
    879 
    880  logging.warning('Writing .gradle files...')
    881  project_entries = []
    882  # When only one entry will be generated we want it to have a valid
    883  # build.gradle file with its own AndroidManifest.
    884  for entry in entries:
    885    data = _GenerateGradleFile(entry, generator, build_vars, jinja_processor)
    886    if data and not args.all:
    887      project_entries.append((entry.ProjectName(), entry.GradleSubdir()))
    888      _WriteFile(
    889          os.path.join(generator.EntryOutputDir(entry), _GRADLE_BUILD_FILE),
    890          data)
    891  if args.all:
    892    project_entries.append((_MODULE_ALL, _MODULE_ALL))
    893    _GenerateModuleAll(_gradle_output_dir, generator, build_vars,
    894                       jinja_processor, args.native_targets)
    895 
    896  root_gradle_path = os.path.join(generator.project_dir, _GRADLE_BUILD_FILE)
    897  _WriteFile(root_gradle_path,
    898             _GenerateRootGradle(jinja_processor, root_gradle_path))
    899 
    900  _WriteFile(os.path.join(generator.project_dir, 'settings.gradle'),
    901             _GenerateSettingsGradle(project_entries))
    902 
    903  # Ensure the Android Studio sdk is correctly initialized.
    904  if not os.path.exists(args.sdk_path):
    905    # Help first-time users avoid Android Studio forcibly changing back to
    906    # the previous default due to not finding a valid sdk under this dir.
    907    shutil.copytree(_RebasePath(build_vars['android_sdk_root']), args.sdk_path)
    908  _WriteFile(
    909      os.path.join(generator.project_dir, 'local.properties'),
    910      _GenerateLocalProperties(args.sdk_path))
    911  _WriteFile(os.path.join(generator.project_dir, 'gradle.properties'),
    912             _GenerateGradleProperties())
    913 
    914  wrapper_properties = os.path.join(generator.project_dir, 'gradle', 'wrapper',
    915                                    'gradle-wrapper.properties')
    916  _WriteFile(wrapper_properties,
    917             _GenerateGradleWrapperProperties(wrapper_properties))
    918 
    919  generated_inputs = set()
    920  for entry in entries:
    921    entries_to_gen = [entry]
    922    entries_to_gen.extend(entry.android_test_entries)
    923    for entry_to_gen in entries_to_gen:
    924      # Build all paths references by .gradle that exist within output_dir.
    925      generated_inputs.update(generator.GeneratedInputs(entry_to_gen))
    926  if generated_inputs:
    927    # Skip targets outside the output_dir since those are not generated.
    928    targets = [
    929        p for p in _RebasePath(generated_inputs, output_dir)
    930        if not p.startswith(os.pardir)
    931    ]
    932    _BuildTargets(output_dir, targets)
    933 
    934  print('Generated projects for Android Studio.')
    935  print('** Building using Android Studio / Gradle does not work.')
    936  print('** This project is only for IDE editing & tools.')
    937  print('Note: Generated files will appear only if they have been built')
    938  print('For more tips: https://chromium.googlesource.com/chromium/src.git/'
    939        '+/main/docs/android_studio.md')
    940 
    941 
    942 if __name__ == '__main__':
    943  main()