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