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