• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
8import argparse
9import codecs
10import collections
11import glob
12import json
13import logging
14import os
15import pathlib
16import re
17import shlex
18import shutil
19import subprocess
20import sys
21
22_BUILD_ANDROID = os.path.join(os.path.dirname(__file__), os.pardir)
23sys.path.append(_BUILD_ANDROID)
24import devil_chromium
25from devil.utils import run_tests_helper
26from pylib import constants
27from pylib.constants import host_paths
28
29sys.path.append(os.path.join(_BUILD_ANDROID, 'gyp'))
30import jinja_template
31from util import build_utils
32from util import resource_utils
33
34sys.path.append(os.path.dirname(_BUILD_ANDROID))
35import 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
76def _TemplatePath(name):
77  return os.path.join(_FILE_DIR, '{}.jinja'.format(name))
78
79
80def _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
99def _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
109def _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
117def _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
124def _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
133class _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
259class _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
376def _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
397def _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
420def _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
460def _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
469def _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
488def _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
501def _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
510def _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
525def _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
536def _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
551def _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
598def _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
636def _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
679def _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
695def _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
712def _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
727def _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
755def 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
942if __name__ == '__main__':
943  main()
944