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