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