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