1#!/usr/bin/env python 2# 3# Copyright 2014 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Writes a build_config file. 8 9The build_config file for a target is a json file containing information about 10how to build that target based on the target's dependencies. This includes 11things like: the javac classpath, the list of android resources dependencies, 12etc. It also includes the information needed to create the build_config for 13other targets that depend on that one. 14 15Android build scripts should not refer to the build_config directly, and the 16build specification should instead pass information in using the special 17file-arg syntax (see build_utils.py:ExpandFileArgs). That syntax allows passing 18of values in a json dict in a file and looks like this: 19 --python-arg=@FileArg(build_config_path:javac:classpath) 20 21Note: If paths to input files are passed in this way, it is important that: 22 1. inputs/deps of the action ensure that the files are available the first 23 time the action runs. 24 2. Either (a) or (b) 25 a. inputs/deps ensure that the action runs whenever one of the files changes 26 b. the files are added to the action's depfile 27""" 28 29import itertools 30import optparse 31import os 32import sys 33import xml.dom.minidom 34 35from util import build_utils 36from util import md5_check 37 38import write_ordered_libraries 39 40 41# Types that should never be used as a dependency of another build config. 42_ROOT_TYPES = ('android_apk', 'deps_dex', 'java_binary', 'resource_rewriter') 43# Types that should not allow code deps to pass through. 44_RESOURCE_TYPES = ('android_assets', 'android_resources') 45 46 47class AndroidManifest(object): 48 def __init__(self, path): 49 self.path = path 50 dom = xml.dom.minidom.parse(path) 51 manifests = dom.getElementsByTagName('manifest') 52 assert len(manifests) == 1 53 self.manifest = manifests[0] 54 55 def GetInstrumentation(self): 56 instrumentation_els = self.manifest.getElementsByTagName('instrumentation') 57 if len(instrumentation_els) == 0: 58 return None 59 if len(instrumentation_els) != 1: 60 raise Exception( 61 'More than one <instrumentation> element found in %s' % self.path) 62 return instrumentation_els[0] 63 64 def CheckInstrumentation(self, expected_package): 65 instr = self.GetInstrumentation() 66 if not instr: 67 raise Exception('No <instrumentation> elements found in %s' % self.path) 68 instrumented_package = instr.getAttributeNS( 69 'http://schemas.android.com/apk/res/android', 'targetPackage') 70 if instrumented_package != expected_package: 71 raise Exception( 72 'Wrong instrumented package. Expected %s, got %s' 73 % (expected_package, instrumented_package)) 74 75 def GetPackageName(self): 76 return self.manifest.getAttribute('package') 77 78 79dep_config_cache = {} 80def GetDepConfig(path): 81 if not path in dep_config_cache: 82 dep_config_cache[path] = build_utils.ReadJson(path)['deps_info'] 83 return dep_config_cache[path] 84 85 86def DepsOfType(wanted_type, configs): 87 return [c for c in configs if c['type'] == wanted_type] 88 89 90def GetAllDepsConfigsInOrder(deps_config_paths): 91 def GetDeps(path): 92 return set(GetDepConfig(path)['deps_configs']) 93 return build_utils.GetSortedTransitiveDependencies(deps_config_paths, GetDeps) 94 95 96def ResolveGroups(configs): 97 while True: 98 groups = DepsOfType('group', configs) 99 if not groups: 100 return configs 101 for config in groups: 102 index = configs.index(config) 103 expanded_configs = [GetDepConfig(p) for p in config['deps_configs']] 104 configs[index:index + 1] = expanded_configs 105 106 107class Deps(object): 108 def __init__(self, direct_deps_config_paths): 109 self.all_deps_config_paths = GetAllDepsConfigsInOrder( 110 direct_deps_config_paths) 111 self.direct_deps_configs = ResolveGroups( 112 [GetDepConfig(p) for p in direct_deps_config_paths]) 113 self.all_deps_configs = [ 114 GetDepConfig(p) for p in self.all_deps_config_paths] 115 self.direct_deps_config_paths = direct_deps_config_paths 116 117 def All(self, wanted_type=None): 118 if type is None: 119 return self.all_deps_configs 120 return DepsOfType(wanted_type, self.all_deps_configs) 121 122 def Direct(self, wanted_type=None): 123 if wanted_type is None: 124 return self.direct_deps_configs 125 return DepsOfType(wanted_type, self.direct_deps_configs) 126 127 def AllConfigPaths(self): 128 return self.all_deps_config_paths 129 130 def RemoveNonDirectDep(self, path): 131 if path in self.direct_deps_config_paths: 132 raise Exception('Cannot remove direct dep.') 133 self.all_deps_config_paths.remove(path) 134 self.all_deps_configs.remove(GetDepConfig(path)) 135 136def _MergeAssets(all_assets): 137 """Merges all assets from the given deps. 138 139 Returns: 140 A tuple of lists: (compressed, uncompressed) 141 Each tuple entry is a list of "srcPath:zipPath". srcPath is the path of the 142 asset to add, and zipPath is the location within the zip (excluding assets/ 143 prefix) 144 """ 145 compressed = {} 146 uncompressed = {} 147 for asset_dep in all_assets: 148 entry = asset_dep['assets'] 149 disable_compression = entry.get('disable_compression', False) 150 dest_map = uncompressed if disable_compression else compressed 151 other_map = compressed if disable_compression else uncompressed 152 outputs = entry.get('outputs', []) 153 for src, dest in itertools.izip_longest(entry['sources'], outputs): 154 if not dest: 155 dest = os.path.basename(src) 156 # Merge so that each path shows up in only one of the lists, and that 157 # deps of the same target override previous ones. 158 other_map.pop(dest, 0) 159 dest_map[dest] = src 160 161 def create_list(asset_map): 162 ret = ['%s:%s' % (src, dest) for dest, src in asset_map.iteritems()] 163 # Sort to ensure deterministic ordering. 164 ret.sort() 165 return ret 166 167 return create_list(compressed), create_list(uncompressed) 168 169 170def _FilterUnwantedDepsPaths(dep_paths, target_type): 171 # Don't allow root targets to be considered as a dep. 172 ret = [p for p in dep_paths if GetDepConfig(p)['type'] not in _ROOT_TYPES] 173 174 # Don't allow java libraries to cross through assets/resources. 175 if target_type in _RESOURCE_TYPES: 176 ret = [p for p in ret if GetDepConfig(p)['type'] in _RESOURCE_TYPES] 177 return ret 178 179 180def _AsInterfaceJar(jar_path): 181 return jar_path[:-3] + 'interface.jar' 182 183 184def main(argv): 185 parser = optparse.OptionParser() 186 build_utils.AddDepfileOption(parser) 187 parser.add_option('--build-config', help='Path to build_config output.') 188 parser.add_option( 189 '--type', 190 help='Type of this target (e.g. android_library).') 191 parser.add_option( 192 '--possible-deps-configs', 193 help='List of paths for dependency\'s build_config files. Some ' 194 'dependencies may not write build_config files. Missing build_config ' 195 'files are handled differently based on the type of this target.') 196 197 # android_resources options 198 parser.add_option('--srcjar', help='Path to target\'s resources srcjar.') 199 parser.add_option('--resources-zip', help='Path to target\'s resources zip.') 200 parser.add_option('--r-text', help='Path to target\'s R.txt file.') 201 parser.add_option('--package-name', 202 help='Java package name for these resources.') 203 parser.add_option('--android-manifest', help='Path to android manifest.') 204 parser.add_option('--is-locale-resource', action='store_true', 205 help='Whether it is locale resource.') 206 207 # android_assets options 208 parser.add_option('--asset-sources', help='List of asset sources.') 209 parser.add_option('--asset-renaming-sources', 210 help='List of asset sources with custom destinations.') 211 parser.add_option('--asset-renaming-destinations', 212 help='List of asset custom destinations.') 213 parser.add_option('--disable-asset-compression', action='store_true', 214 help='Whether to disable asset compression.') 215 216 # java library options 217 parser.add_option('--jar-path', help='Path to target\'s jar output.') 218 parser.add_option('--supports-android', action='store_true', 219 help='Whether this library supports running on the Android platform.') 220 parser.add_option('--requires-android', action='store_true', 221 help='Whether this library requires running on the Android platform.') 222 parser.add_option('--bypass-platform-checks', action='store_true', 223 help='Bypass checks for support/require Android platform.') 224 225 # android library options 226 parser.add_option('--dex-path', help='Path to target\'s dex output.') 227 228 # native library options 229 parser.add_option('--native-libs', help='List of top-level native libs.') 230 parser.add_option('--readelf-path', help='Path to toolchain\'s readelf.') 231 232 # apk options 233 parser.add_option('--apk-path', help='Path to the target\'s apk output.') 234 parser.add_option('--incremental-apk-path', 235 help="Path to the target's incremental apk output.") 236 parser.add_option('--incremental-install-script-path', 237 help="Path to the target's generated incremental install " 238 "script.") 239 240 parser.add_option('--tested-apk-config', 241 help='Path to the build config of the tested apk (for an instrumentation ' 242 'test apk).') 243 parser.add_option('--proguard-enabled', action='store_true', 244 help='Whether proguard is enabled for this apk.') 245 parser.add_option('--proguard-info', 246 help='Path to the proguard .info output for this apk.') 247 parser.add_option('--has-alternative-locale-resource', action='store_true', 248 help='Whether there is alternative-locale-resource in direct deps') 249 250 options, args = parser.parse_args(argv) 251 252 if args: 253 parser.error('No positional arguments should be given.') 254 255 required_options_map = { 256 'java_binary': ['build_config', 'jar_path'], 257 'java_library': ['build_config', 'jar_path'], 258 'android_assets': ['build_config'], 259 'android_resources': ['build_config', 'resources_zip'], 260 'android_apk': ['build_config', 'jar_path', 'dex_path', 'resources_zip'], 261 'deps_dex': ['build_config', 'dex_path'], 262 'resource_rewriter': ['build_config'], 263 'group': ['build_config'], 264 } 265 required_options = required_options_map.get(options.type) 266 if not required_options: 267 raise Exception('Unknown type: <%s>' % options.type) 268 269 if options.native_libs: 270 required_options.append('readelf_path') 271 272 build_utils.CheckOptions(options, parser, required_options) 273 274 if options.type == 'java_library': 275 if options.supports_android and not options.dex_path: 276 raise Exception('java_library that supports Android requires a dex path.') 277 278 if options.requires_android and not options.supports_android: 279 raise Exception( 280 '--supports-android is required when using --requires-android') 281 282 possible_deps_config_paths = build_utils.ParseGypList( 283 options.possible_deps_configs) 284 285 unknown_deps = [ 286 c for c in possible_deps_config_paths if not os.path.exists(c)] 287 288 direct_deps_config_paths = [ 289 c for c in possible_deps_config_paths if not c in unknown_deps] 290 direct_deps_config_paths = _FilterUnwantedDepsPaths(direct_deps_config_paths, 291 options.type) 292 293 deps = Deps(direct_deps_config_paths) 294 all_inputs = deps.AllConfigPaths() + build_utils.GetPythonDependencies() 295 296 # Remove other locale resources if there is alternative_locale_resource in 297 # direct deps. 298 if options.has_alternative_locale_resource: 299 alternative = [r['path'] for r in deps.Direct('android_resources') 300 if r.get('is_locale_resource')] 301 # We can only have one locale resources in direct deps. 302 if len(alternative) != 1: 303 raise Exception('The number of locale resource in direct deps is wrong %d' 304 % len(alternative)) 305 unwanted = [r['path'] for r in deps.All('android_resources') 306 if r.get('is_locale_resource') and r['path'] not in alternative] 307 for p in unwanted: 308 deps.RemoveNonDirectDep(p) 309 310 311 direct_library_deps = deps.Direct('java_library') 312 all_library_deps = deps.All('java_library') 313 314 direct_resources_deps = deps.Direct('android_resources') 315 all_resources_deps = deps.All('android_resources') 316 # Resources should be ordered with the highest-level dependency first so that 317 # overrides are done correctly. 318 all_resources_deps.reverse() 319 320 if options.type == 'android_apk' and options.tested_apk_config: 321 tested_apk_deps = Deps([options.tested_apk_config]) 322 tested_apk_resources_deps = tested_apk_deps.All('android_resources') 323 all_resources_deps = [ 324 d for d in all_resources_deps if not d in tested_apk_resources_deps] 325 326 # Initialize some common config. 327 config = { 328 'deps_info': { 329 'name': os.path.basename(options.build_config), 330 'path': options.build_config, 331 'type': options.type, 332 'deps_configs': direct_deps_config_paths 333 } 334 } 335 deps_info = config['deps_info'] 336 337 if (options.type in ('java_binary', 'java_library') and 338 not options.bypass_platform_checks): 339 deps_info['requires_android'] = options.requires_android 340 deps_info['supports_android'] = options.supports_android 341 342 deps_require_android = (all_resources_deps + 343 [d['name'] for d in all_library_deps if d['requires_android']]) 344 deps_not_support_android = ( 345 [d['name'] for d in all_library_deps if not d['supports_android']]) 346 347 if deps_require_android and not options.requires_android: 348 raise Exception('Some deps require building for the Android platform: ' + 349 str(deps_require_android)) 350 351 if deps_not_support_android and options.supports_android: 352 raise Exception('Not all deps support the Android platform: ' + 353 str(deps_not_support_android)) 354 355 if options.type in ('java_binary', 'java_library', 'android_apk'): 356 javac_classpath = [c['jar_path'] for c in direct_library_deps] 357 java_full_classpath = [c['jar_path'] for c in all_library_deps] 358 deps_info['resources_deps'] = [c['path'] for c in all_resources_deps] 359 deps_info['jar_path'] = options.jar_path 360 if options.type == 'android_apk' or options.supports_android: 361 deps_info['dex_path'] = options.dex_path 362 if options.type == 'android_apk': 363 deps_info['apk_path'] = options.apk_path 364 deps_info['incremental_apk_path'] = options.incremental_apk_path 365 deps_info['incremental_install_script_path'] = ( 366 options.incremental_install_script_path) 367 368 # Classpath values filled in below (after applying tested_apk_config). 369 config['javac'] = {} 370 371 if options.type in ('java_binary', 'java_library'): 372 # Only resources might have srcjars (normal srcjar targets are listed in 373 # srcjar_deps). A resource's srcjar contains the R.java file for those 374 # resources, and (like Android's default build system) we allow a library to 375 # refer to the resources in any of its dependents. 376 config['javac']['srcjars'] = [ 377 c['srcjar'] for c in direct_resources_deps if 'srcjar' in c] 378 379 # Used to strip out R.class for android_prebuilt()s. 380 if options.type == 'java_library': 381 config['javac']['resource_packages'] = [ 382 c['package_name'] for c in all_resources_deps if 'package_name' in c] 383 384 if options.type == 'android_apk': 385 # Apks will get their resources srcjar explicitly passed to the java step. 386 config['javac']['srcjars'] = [] 387 388 if options.type == 'android_assets': 389 all_asset_sources = [] 390 if options.asset_renaming_sources: 391 all_asset_sources.extend( 392 build_utils.ParseGypList(options.asset_renaming_sources)) 393 if options.asset_sources: 394 all_asset_sources.extend(build_utils.ParseGypList(options.asset_sources)) 395 396 deps_info['assets'] = { 397 'sources': all_asset_sources 398 } 399 if options.asset_renaming_destinations: 400 deps_info['assets']['outputs'] = ( 401 build_utils.ParseGypList(options.asset_renaming_destinations)) 402 if options.disable_asset_compression: 403 deps_info['assets']['disable_compression'] = True 404 405 if options.type == 'android_resources': 406 deps_info['resources_zip'] = options.resources_zip 407 if options.srcjar: 408 deps_info['srcjar'] = options.srcjar 409 if options.android_manifest: 410 manifest = AndroidManifest(options.android_manifest) 411 deps_info['package_name'] = manifest.GetPackageName() 412 if options.package_name: 413 deps_info['package_name'] = options.package_name 414 if options.r_text: 415 deps_info['r_text'] = options.r_text 416 if options.is_locale_resource: 417 deps_info['is_locale_resource'] = True 418 419 if options.type in ('android_resources','android_apk', 'resource_rewriter'): 420 config['resources'] = {} 421 config['resources']['dependency_zips'] = [ 422 c['resources_zip'] for c in all_resources_deps] 423 config['resources']['extra_package_names'] = [] 424 config['resources']['extra_r_text_files'] = [] 425 426 if options.type == 'android_apk' or options.type == 'resource_rewriter': 427 config['resources']['extra_package_names'] = [ 428 c['package_name'] for c in all_resources_deps if 'package_name' in c] 429 config['resources']['extra_r_text_files'] = [ 430 c['r_text'] for c in all_resources_deps if 'r_text' in c] 431 432 if options.type in ['android_apk', 'deps_dex']: 433 deps_dex_files = [c['dex_path'] for c in all_library_deps] 434 435 proguard_enabled = options.proguard_enabled 436 if options.type == 'android_apk': 437 deps_info['proguard_enabled'] = proguard_enabled 438 439 if proguard_enabled: 440 deps_info['proguard_info'] = options.proguard_info 441 config['proguard'] = {} 442 proguard_config = config['proguard'] 443 proguard_config['input_paths'] = [options.jar_path] + java_full_classpath 444 445 # An instrumentation test apk should exclude the dex files that are in the apk 446 # under test. 447 if options.type == 'android_apk' and options.tested_apk_config: 448 tested_apk_library_deps = tested_apk_deps.All('java_library') 449 tested_apk_deps_dex_files = [c['dex_path'] for c in tested_apk_library_deps] 450 # Include in the classpath classes that are added directly to the apk under 451 # test (those that are not a part of a java_library). 452 tested_apk_config = GetDepConfig(options.tested_apk_config) 453 javac_classpath.append(tested_apk_config['jar_path']) 454 # Exclude dex files from the test apk that exist within the apk under test. 455 deps_dex_files = [ 456 p for p in deps_dex_files if not p in tested_apk_deps_dex_files] 457 458 expected_tested_package = tested_apk_config['package_name'] 459 AndroidManifest(options.android_manifest).CheckInstrumentation( 460 expected_tested_package) 461 if tested_apk_config['proguard_enabled']: 462 assert proguard_enabled, ('proguard must be enabled for instrumentation' 463 ' apks if it\'s enabled for the tested apk') 464 465 # Dependencies for the final dex file of an apk or a 'deps_dex'. 466 if options.type in ['android_apk', 'deps_dex']: 467 config['final_dex'] = {} 468 dex_config = config['final_dex'] 469 dex_config['dependency_dex_files'] = deps_dex_files 470 471 if options.type in ('java_binary', 'java_library', 'android_apk'): 472 config['javac']['classpath'] = javac_classpath 473 config['javac']['interface_classpath'] = [ 474 _AsInterfaceJar(p) for p in javac_classpath] 475 config['java'] = { 476 'full_classpath': java_full_classpath 477 } 478 479 if options.type == 'android_apk': 480 dependency_jars = [c['jar_path'] for c in all_library_deps] 481 all_interface_jars = [ 482 _AsInterfaceJar(p) for p in dependency_jars + [options.jar_path]] 483 config['dist_jar'] = { 484 'dependency_jars': dependency_jars, 485 'all_interface_jars': all_interface_jars, 486 } 487 manifest = AndroidManifest(options.android_manifest) 488 deps_info['package_name'] = manifest.GetPackageName() 489 if not options.tested_apk_config and manifest.GetInstrumentation(): 490 # This must then have instrumentation only for itself. 491 manifest.CheckInstrumentation(manifest.GetPackageName()) 492 493 library_paths = [] 494 java_libraries_list_holder = [None] 495 libraries = build_utils.ParseGypList(options.native_libs or '[]') 496 if libraries: 497 def recompute_ordered_libraries(): 498 libraries_dir = os.path.dirname(libraries[0]) 499 write_ordered_libraries.SetReadelfPath(options.readelf_path) 500 write_ordered_libraries.SetLibraryDirs([libraries_dir]) 501 all_deps = ( 502 write_ordered_libraries.GetSortedTransitiveDependenciesForBinaries( 503 libraries)) 504 # Create a java literal array with the "base" library names: 505 # e.g. libfoo.so -> foo 506 java_libraries_list_holder[0] = ('{%s}' % ','.join( 507 ['"%s"' % s[3:-3] for s in all_deps])) 508 library_paths.extend( 509 write_ordered_libraries.FullLibraryPath(x) for x in all_deps) 510 511 # This step takes about 600ms on a z620 for chrome_apk, so it's worth 512 # caching. 513 md5_check.CallAndRecordIfStale( 514 recompute_ordered_libraries, 515 record_path=options.build_config + '.nativelibs.md5.stamp', 516 input_paths=libraries, 517 output_paths=[options.build_config]) 518 if not library_paths: 519 prev_config = build_utils.ReadJson(options.build_config) 520 java_libraries_list_holder[0] = ( 521 prev_config['native']['java_libraries_list']) 522 library_paths.extend(prev_config['native']['libraries']) 523 524 all_inputs.extend(library_paths) 525 config['native'] = { 526 'libraries': library_paths, 527 'java_libraries_list': java_libraries_list_holder[0], 528 } 529 config['assets'], config['uncompressed_assets'] = ( 530 _MergeAssets(deps.All('android_assets'))) 531 532 build_utils.WriteJson(config, options.build_config, only_if_changed=True) 533 534 if options.depfile: 535 build_utils.WriteDepfile(options.depfile, all_inputs) 536 537 538if __name__ == '__main__': 539 sys.exit(main(sys.argv[1:])) 540