1#!/usr/bin/env python3 2# 3# Copyright 2012 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Compile Android resources into an intermediate APK. 8 9This can also generate an R.txt, and an .srcjar file containing the proper 10final R.java class for all resource packages the APK depends on. 11 12This will crunch images with aapt2. 13""" 14 15import argparse 16import collections 17import contextlib 18import filecmp 19import hashlib 20import logging 21import os 22import pathlib 23import re 24import shutil 25import subprocess 26import sys 27import textwrap 28from xml.etree import ElementTree 29 30from util import build_utils 31from util import diff_utils 32from util import manifest_utils 33from util import parallel 34from util import protoresources 35from util import resource_utils 36import action_helpers # build_utils adds //build to sys.path. 37import zip_helpers 38 39 40# Pngs that we shouldn't convert to webp. Please add rationale when updating. 41_PNG_WEBP_EXCLUSION_PATTERN = re.compile('|'.join([ 42 # Android requires pngs for 9-patch images. 43 r'.*\.9\.png', 44 # Daydream requires pngs for icon files. 45 r'.*daydream_icon_.*\.png' 46])) 47 48 49def _ParseArgs(args): 50 """Parses command line options. 51 52 Returns: 53 An options object as from argparse.ArgumentParser.parse_args() 54 """ 55 parser = argparse.ArgumentParser(description=__doc__) 56 57 input_opts = parser.add_argument_group('Input options') 58 output_opts = parser.add_argument_group('Output options') 59 60 input_opts.add_argument('--include-resources', 61 action='append', 62 required=True, 63 help='Paths to arsc resource files used to link ' 64 'against. Can be specified multiple times.') 65 input_opts.add_argument( 66 '--dependencies-res-zips', 67 default=[], 68 help='Resources zip archives from dependents. Required to ' 69 'resolve @type/foo references into dependent libraries.') 70 input_opts.add_argument( 71 '--extra-res-packages', 72 help='Additional package names to generate R.java files for.') 73 input_opts.add_argument( 74 '--aapt2-path', required=True, help='Path to the Android aapt2 tool.') 75 input_opts.add_argument( 76 '--android-manifest', required=True, help='AndroidManifest.xml path.') 77 input_opts.add_argument( 78 '--r-java-root-package-name', 79 default='base', 80 help='Short package name for this target\'s root R java file (ex. ' 81 'input of "base" would become gen.base_module). Defaults to "base".') 82 group = input_opts.add_mutually_exclusive_group() 83 group.add_argument( 84 '--shared-resources', 85 action='store_true', 86 help='Make all resources in R.java non-final and allow the resource IDs ' 87 'to be reset to a different package index when the apk is loaded by ' 88 'another application at runtime.') 89 group.add_argument( 90 '--app-as-shared-lib', 91 action='store_true', 92 help='Same as --shared-resources, but also ensures all resource IDs are ' 93 'directly usable from the APK loaded as an application.') 94 input_opts.add_argument( 95 '--package-id', 96 type=int, 97 help='Decimal integer representing custom package ID for resources ' 98 '(instead of 127==0x7f). Cannot be used with --shared-resources.') 99 input_opts.add_argument( 100 '--package-name', 101 help='Package name that will be used to create R class.') 102 input_opts.add_argument( 103 '--rename-manifest-package', help='Package name to force AAPT to use.') 104 input_opts.add_argument( 105 '--arsc-package-name', 106 help='Package name to set in manifest of resources.arsc file. This is ' 107 'only used for apks under test.') 108 input_opts.add_argument( 109 '--shared-resources-allowlist', 110 help='An R.txt file acting as a allowlist for resources that should be ' 111 'non-final and have their package ID changed at runtime in R.java. ' 112 'Implies and overrides --shared-resources.') 113 input_opts.add_argument( 114 '--shared-resources-allowlist-locales', 115 default='[]', 116 help='Optional GN-list of locales. If provided, all strings corresponding' 117 ' to this locale list will be kept in the final output for the ' 118 'resources identified through --shared-resources-allowlist, even ' 119 'if --locale-allowlist is being used.') 120 input_opts.add_argument( 121 '--use-resource-ids-path', 122 help='Use resource IDs generated by aapt --emit-ids.') 123 input_opts.add_argument( 124 '--debuggable', 125 action='store_true', 126 help='Whether to add android:debuggable="true".') 127 input_opts.add_argument('--static-library-version', 128 help='Version code for static library.') 129 input_opts.add_argument('--version-code', help='Version code for apk.') 130 input_opts.add_argument('--version-name', help='Version name for apk.') 131 input_opts.add_argument( 132 '--min-sdk-version', required=True, help='android:minSdkVersion for APK.') 133 input_opts.add_argument( 134 '--target-sdk-version', 135 required=True, 136 help="android:targetSdkVersion for APK.") 137 input_opts.add_argument( 138 '--max-sdk-version', 139 help="android:maxSdkVersion expected in AndroidManifest.xml.") 140 input_opts.add_argument( 141 '--manifest-package', help='Package name of the AndroidManifest.xml.') 142 input_opts.add_argument( 143 '--locale-allowlist', 144 default='[]', 145 help='GN list of languages to include. All other language configs will ' 146 'be stripped out. List may include a combination of Android locales ' 147 'or Chrome locales.') 148 input_opts.add_argument( 149 '--resource-exclusion-regex', 150 default='', 151 help='File-based filter for resources (applied before compiling)') 152 input_opts.add_argument( 153 '--resource-exclusion-exceptions', 154 default='[]', 155 help='GN list of globs that say which files to include even ' 156 'when --resource-exclusion-regex is set.') 157 input_opts.add_argument( 158 '--dependencies-res-zip-overlays', 159 help='GN list with subset of --dependencies-res-zips to use overlay ' 160 'semantics for.') 161 input_opts.add_argument( 162 '--values-filter-rules', 163 help='GN list of source_glob:regex for filtering resources after they ' 164 'are compiled. Use this to filter out entries within values/ files.') 165 input_opts.add_argument('--png-to-webp', action='store_true', 166 help='Convert png files to webp format.') 167 168 input_opts.add_argument('--webp-binary', default='', 169 help='Path to the cwebp binary.') 170 input_opts.add_argument( 171 '--webp-cache-dir', help='The directory to store webp image cache.') 172 input_opts.add_argument( 173 '--is-bundle-module', 174 action='store_true', 175 help='Whether resources are being generated for a bundle module.') 176 input_opts.add_argument( 177 '--uses-split', 178 help='Value to set uses-split to in the AndroidManifest.xml.') 179 input_opts.add_argument( 180 '--verification-version-code-offset', 181 help='Subtract this from versionCode for expectation files') 182 input_opts.add_argument( 183 '--verification-library-version-offset', 184 help='Subtract this from static-library version for expectation files') 185 186 action_helpers.add_depfile_arg(output_opts) 187 output_opts.add_argument('--arsc-path', help='Apk output for arsc format.') 188 output_opts.add_argument('--proto-path', help='Apk output for proto format.') 189 output_opts.add_argument( 190 '--info-path', help='Path to output info file for the partial apk.') 191 output_opts.add_argument( 192 '--srcjar-out', 193 help='Path to srcjar to contain generated R.java.') 194 output_opts.add_argument('--r-text-out', 195 help='Path to store the generated R.txt file.') 196 output_opts.add_argument( 197 '--proguard-file', help='Path to proguard.txt generated file.') 198 output_opts.add_argument( 199 '--emit-ids-out', help='Path to file produced by aapt2 --emit-ids.') 200 201 diff_utils.AddCommandLineFlags(parser) 202 options = parser.parse_args(args) 203 204 options.include_resources = action_helpers.parse_gn_list( 205 options.include_resources) 206 options.dependencies_res_zips = action_helpers.parse_gn_list( 207 options.dependencies_res_zips) 208 options.extra_res_packages = action_helpers.parse_gn_list( 209 options.extra_res_packages) 210 options.locale_allowlist = action_helpers.parse_gn_list( 211 options.locale_allowlist) 212 options.shared_resources_allowlist_locales = action_helpers.parse_gn_list( 213 options.shared_resources_allowlist_locales) 214 options.resource_exclusion_exceptions = action_helpers.parse_gn_list( 215 options.resource_exclusion_exceptions) 216 options.dependencies_res_zip_overlays = action_helpers.parse_gn_list( 217 options.dependencies_res_zip_overlays) 218 options.values_filter_rules = action_helpers.parse_gn_list( 219 options.values_filter_rules) 220 221 if not options.arsc_path and not options.proto_path: 222 parser.error('One of --arsc-path or --proto-path is required.') 223 224 if options.package_id and options.shared_resources: 225 parser.error('--package-id and --shared-resources are mutually exclusive') 226 227 if options.static_library_version and (options.static_library_version != 228 options.version_code): 229 assert options.static_library_version == options.version_code, ( 230 f'static_library_version={options.static_library_version} must equal ' 231 f'version_code={options.version_code}. Please verify the version code ' 232 'map for this target is defined correctly.') 233 234 return options 235 236 237def _IterFiles(root_dir): 238 for root, _, files in os.walk(root_dir): 239 for f in files: 240 yield os.path.join(root, f) 241 242 243def _RenameLocaleResourceDirs(resource_dirs, path_info): 244 """Rename locale resource directories into standard names when necessary. 245 246 This is necessary to deal with the fact that older Android releases only 247 support ISO 639-1 two-letter codes, and sometimes even obsolete versions 248 of them. 249 250 In practice it means: 251 * 3-letter ISO 639-2 qualifiers are renamed under a corresponding 252 2-letter one. E.g. for Filipino, strings under values-fil/ will be moved 253 to a new corresponding values-tl/ sub-directory. 254 255 * Modern ISO 639-1 codes will be renamed to their obsolete variant 256 for Indonesian, Hebrew and Yiddish (e.g. 'values-in/ -> values-id/). 257 258 * Norwegian macrolanguage strings will be renamed to Bokmal (main 259 Norway language). See http://crbug.com/920960. In practice this 260 means that 'values-no/ -> values-nb/' unless 'values-nb/' already 261 exists. 262 263 * BCP 47 langauge tags will be renamed to an equivalent ISO 639-1 264 locale qualifier if possible (e.g. 'values-b+en+US/ -> values-en-rUS'). 265 266 Args: 267 resource_dirs: list of top-level resource directories. 268 """ 269 for resource_dir in resource_dirs: 270 ignore_dirs = {} 271 for path in _IterFiles(resource_dir): 272 locale = resource_utils.FindLocaleInStringResourceFilePath(path) 273 if not locale: 274 continue 275 cr_locale = resource_utils.ToChromiumLocaleName(locale) 276 if not cr_locale: 277 continue # Unsupported Android locale qualifier!? 278 locale2 = resource_utils.ToAndroidLocaleName(cr_locale) 279 if locale != locale2: 280 path2 = path.replace('/values-%s/' % locale, '/values-%s/' % locale2) 281 if path == path2: 282 raise Exception('Could not substitute locale %s for %s in %s' % 283 (locale, locale2, path)) 284 285 # Ignore rather than rename when the destination resources config 286 # already exists. 287 # e.g. some libraries provide both values-nb/ and values-no/. 288 # e.g. material design provides: 289 # * res/values-rUS/values-rUS.xml 290 # * res/values-b+es+419/values-b+es+419.xml 291 config_dir = os.path.dirname(path2) 292 already_has_renamed_config = ignore_dirs.get(config_dir) 293 if already_has_renamed_config is None: 294 # Cache the result of the first time the directory is encountered 295 # since subsequent encounters will find the directory already exists 296 # (due to the rename). 297 already_has_renamed_config = os.path.exists(config_dir) 298 ignore_dirs[config_dir] = already_has_renamed_config 299 if already_has_renamed_config: 300 continue 301 302 build_utils.MakeDirectory(os.path.dirname(path2)) 303 shutil.move(path, path2) 304 path_info.RegisterRename( 305 os.path.relpath(path, resource_dir), 306 os.path.relpath(path2, resource_dir)) 307 308 309def _ToAndroidLocales(locale_allowlist): 310 """Converts the list of Chrome locales to Android config locale qualifiers. 311 312 Args: 313 locale_allowlist: A list of Chromium locale names. 314 Returns: 315 A set of matching Android config locale qualifier names. 316 """ 317 ret = set() 318 for locale in locale_allowlist: 319 locale = resource_utils.ToAndroidLocaleName(locale) 320 if locale is None or ('-' in locale and '-r' not in locale): 321 raise Exception('Unsupported Chromium locale name: %s' % locale) 322 ret.add(locale) 323 # Always keep non-regional fall-backs. 324 language = locale.split('-')[0] 325 ret.add(language) 326 327 return ret 328 329 330def _MoveImagesToNonMdpiFolders(res_root, path_info): 331 """Move images from drawable-*-mdpi-* folders to drawable-* folders. 332 333 Why? http://crbug.com/289843 334 """ 335 for src_dir_name in os.listdir(res_root): 336 src_components = src_dir_name.split('-') 337 if src_components[0] != 'drawable' or 'mdpi' not in src_components: 338 continue 339 src_dir = os.path.join(res_root, src_dir_name) 340 if not os.path.isdir(src_dir): 341 continue 342 dst_components = [c for c in src_components if c != 'mdpi'] 343 assert dst_components != src_components 344 dst_dir_name = '-'.join(dst_components) 345 dst_dir = os.path.join(res_root, dst_dir_name) 346 build_utils.MakeDirectory(dst_dir) 347 for src_file_name in os.listdir(src_dir): 348 src_file = os.path.join(src_dir, src_file_name) 349 dst_file = os.path.join(dst_dir, src_file_name) 350 assert not os.path.lexists(dst_file) 351 shutil.move(src_file, dst_file) 352 path_info.RegisterRename( 353 os.path.relpath(src_file, res_root), 354 os.path.relpath(dst_file, res_root)) 355 356 357def _DeterminePlatformVersion(aapt2_path, jar_candidates): 358 def maybe_extract_version(j): 359 try: 360 return resource_utils.ExtractBinaryManifestValues(aapt2_path, j) 361 except build_utils.CalledProcessError: 362 return None 363 364 def is_sdk_jar(jar_name): 365 if jar_name in ('android.jar', 'android_system.jar'): 366 return True 367 # Robolectric jar looks a bit different. 368 return 'android-all' in jar_name and 'robolectric' in jar_name 369 370 android_sdk_jars = [ 371 j for j in jar_candidates if is_sdk_jar(os.path.basename(j)) 372 ] 373 extract_all = [maybe_extract_version(j) for j in android_sdk_jars] 374 extract_all = [x for x in extract_all if x] 375 if len(extract_all) == 0: 376 raise Exception( 377 'Unable to find android SDK jar among candidates: %s' 378 % ', '.join(android_sdk_jars)) 379 if len(extract_all) > 1: 380 raise Exception( 381 'Found multiple android SDK jars among candidates: %s' 382 % ', '.join(android_sdk_jars)) 383 platform_version_code, platform_version_name = extract_all.pop()[:2] 384 return platform_version_code, platform_version_name 385 386 387def _FixManifest(options, temp_dir): 388 """Fix the APK's AndroidManifest.xml. 389 390 This adds any missing namespaces for 'android' and 'tools', and 391 sets certains elements like 'platformBuildVersionCode' or 392 'android:debuggable' depending on the content of |options|. 393 394 Args: 395 options: The command-line arguments tuple. 396 temp_dir: A temporary directory where the fixed manifest will be written to. 397 Returns: 398 Tuple of: 399 * Manifest path within |temp_dir|. 400 * Original package_name. 401 * Manifest package name. 402 """ 403 doc, manifest_node, app_node = manifest_utils.ParseManifest( 404 options.android_manifest) 405 406 # merge_manifest.py also sets package & <uses-sdk>. We may want to ensure 407 # manifest merger is always enabled and remove these command-line arguments. 408 manifest_utils.SetUsesSdk(manifest_node, options.target_sdk_version, 409 options.min_sdk_version, options.max_sdk_version) 410 orig_package = manifest_node.get('package') or options.manifest_package 411 fixed_package = (options.arsc_package_name or options.manifest_package 412 or orig_package) 413 manifest_node.set('package', fixed_package) 414 415 platform_version_code, platform_version_name = _DeterminePlatformVersion( 416 options.aapt2_path, options.include_resources) 417 manifest_node.set('platformBuildVersionCode', platform_version_code) 418 manifest_node.set('platformBuildVersionName', platform_version_name) 419 if options.version_code: 420 manifest_utils.NamespacedSet(manifest_node, 'versionCode', 421 options.version_code) 422 if options.version_name: 423 manifest_utils.NamespacedSet(manifest_node, 'versionName', 424 options.version_name) 425 if options.debuggable: 426 manifest_utils.NamespacedSet(app_node, 'debuggable', 'true') 427 428 if options.uses_split: 429 uses_split = ElementTree.SubElement(manifest_node, 'uses-split') 430 manifest_utils.NamespacedSet(uses_split, 'name', options.uses_split) 431 432 # Make sure the min-sdk condition is not less than the min-sdk of the bundle. 433 for min_sdk_node in manifest_node.iter('{%s}min-sdk' % 434 manifest_utils.DIST_NAMESPACE): 435 dist_value = '{%s}value' % manifest_utils.DIST_NAMESPACE 436 if int(min_sdk_node.get(dist_value)) < int(options.min_sdk_version): 437 min_sdk_node.set(dist_value, options.min_sdk_version) 438 439 debug_manifest_path = os.path.join(temp_dir, 'AndroidManifest.xml') 440 manifest_utils.SaveManifest(doc, debug_manifest_path) 441 return debug_manifest_path, orig_package, fixed_package 442 443 444def _CreateKeepPredicate(resource_exclusion_regex, 445 resource_exclusion_exceptions): 446 """Return a predicate lambda to determine which resource files to keep. 447 448 Args: 449 resource_exclusion_regex: A regular expression describing all resources 450 to exclude, except if they are mip-maps, or if they are listed 451 in |resource_exclusion_exceptions|. 452 resource_exclusion_exceptions: A list of glob patterns corresponding 453 to exceptions to the |resource_exclusion_regex|. 454 Returns: 455 A lambda that takes a path, and returns true if the corresponding file 456 must be kept. 457 """ 458 predicate = lambda path: os.path.basename(path)[0] != '.' 459 if resource_exclusion_regex == '': 460 # Do not extract dotfiles (e.g. ".gitkeep"). aapt ignores them anyways. 461 return predicate 462 463 # A simple predicate that only removes (returns False for) paths covered by 464 # the exclusion regex or listed as exceptions. 465 return lambda path: ( 466 not re.search(resource_exclusion_regex, path) or 467 build_utils.MatchesGlob(path, resource_exclusion_exceptions)) 468 469 470def _ComputeSha1(path): 471 with open(path, 'rb') as f: 472 data = f.read() 473 return hashlib.sha1(data).hexdigest() 474 475 476def _ConvertToWebPSingle(png_path, cwebp_binary, cwebp_version, webp_cache_dir): 477 sha1_hash = _ComputeSha1(png_path) 478 479 # The set of arguments that will appear in the cache key. 480 quality_args = ['-m', '6', '-q', '100', '-lossless'] 481 482 webp_cache_path = os.path.join( 483 webp_cache_dir, '{}-{}-{}'.format(sha1_hash, cwebp_version, 484 ''.join(quality_args))) 485 # No need to add .webp. Android can load images fine without them. 486 webp_path = os.path.splitext(png_path)[0] 487 488 cache_hit = os.path.exists(webp_cache_path) 489 if cache_hit: 490 os.link(webp_cache_path, webp_path) 491 else: 492 # We place the generated webp image to webp_path, instead of in the 493 # webp_cache_dir to avoid concurrency issues. 494 args = [cwebp_binary, png_path, '-o', webp_path, '-quiet'] + quality_args 495 subprocess.check_call(args) 496 497 try: 498 os.link(webp_path, webp_cache_path) 499 except OSError: 500 # Because of concurrent run, a webp image may already exists in 501 # webp_cache_path. 502 pass 503 504 os.remove(png_path) 505 original_dir = os.path.dirname(os.path.dirname(png_path)) 506 rename_tuple = (os.path.relpath(png_path, original_dir), 507 os.path.relpath(webp_path, original_dir)) 508 return rename_tuple, cache_hit 509 510 511def _ConvertToWebP(cwebp_binary, png_paths, path_info, webp_cache_dir): 512 cwebp_version = subprocess.check_output([cwebp_binary, '-version']).rstrip() 513 shard_args = [(f, ) for f in png_paths 514 if not _PNG_WEBP_EXCLUSION_PATTERN.match(f)] 515 516 build_utils.MakeDirectory(webp_cache_dir) 517 results = parallel.BulkForkAndCall(_ConvertToWebPSingle, 518 shard_args, 519 cwebp_binary=cwebp_binary, 520 cwebp_version=cwebp_version, 521 webp_cache_dir=webp_cache_dir) 522 total_cache_hits = 0 523 for rename_tuple, cache_hit in results: 524 path_info.RegisterRename(*rename_tuple) 525 total_cache_hits += int(cache_hit) 526 527 logging.debug('png->webp cache: %d/%d', total_cache_hits, len(shard_args)) 528 529 530def _RemoveImageExtensions(directory, path_info): 531 """Remove extensions from image files in the passed directory. 532 533 This reduces binary size but does not affect android's ability to load the 534 images. 535 """ 536 for f in _IterFiles(directory): 537 if (f.endswith('.png') or f.endswith('.webp')) and not f.endswith('.9.png'): 538 path_with_extension = f 539 path_no_extension = os.path.splitext(path_with_extension)[0] 540 if path_no_extension != path_with_extension: 541 shutil.move(path_with_extension, path_no_extension) 542 path_info.RegisterRename( 543 os.path.relpath(path_with_extension, directory), 544 os.path.relpath(path_no_extension, directory)) 545 546 547def _CompileSingleDep(index, dep_subdir, keep_predicate, aapt2_path, 548 partials_dir): 549 unique_name = '{}_{}'.format(index, os.path.basename(dep_subdir)) 550 partial_path = os.path.join(partials_dir, '{}.zip'.format(unique_name)) 551 552 compile_command = [ 553 aapt2_path, 554 'compile', 555 # TODO(wnwen): Turn this on once aapt2 forces 9-patch to be crunched. 556 # '--no-crunch', 557 '--dir', 558 dep_subdir, 559 '-o', 560 partial_path 561 ] 562 563 # There are resources targeting API-versions lower than our minapi. For 564 # various reasons it's easier to let aapt2 ignore these than for us to 565 # remove them from our build (e.g. it's from a 3rd party library). 566 build_utils.CheckOutput( 567 compile_command, 568 stderr_filter=lambda output: build_utils.FilterLines( 569 output, r'ignoring configuration .* for (styleable|attribute)')) 570 571 # Filtering these files is expensive, so only apply filters to the partials 572 # that have been explicitly targeted. 573 if keep_predicate: 574 logging.debug('Applying .arsc filtering to %s', dep_subdir) 575 protoresources.StripUnwantedResources(partial_path, keep_predicate) 576 return partial_path 577 578 579def _CreateValuesKeepPredicate(exclusion_rules, dep_subdir): 580 patterns = [ 581 x[1] for x in exclusion_rules 582 if build_utils.MatchesGlob(dep_subdir, [x[0]]) 583 ] 584 if not patterns: 585 return None 586 587 regexes = [re.compile(p) for p in patterns] 588 return lambda x: not any(r.search(x) for r in regexes) 589 590 591def _CompileDeps(aapt2_path, dep_subdirs, dep_subdir_overlay_set, temp_dir, 592 exclusion_rules): 593 partials_dir = os.path.join(temp_dir, 'partials') 594 build_utils.MakeDirectory(partials_dir) 595 596 job_params = [(i, dep_subdir, 597 _CreateValuesKeepPredicate(exclusion_rules, dep_subdir)) 598 for i, dep_subdir in enumerate(dep_subdirs)] 599 600 # Filtering is slow, so ensure jobs with keep_predicate are started first. 601 job_params.sort(key=lambda x: not x[2]) 602 partials = list( 603 parallel.BulkForkAndCall(_CompileSingleDep, 604 job_params, 605 aapt2_path=aapt2_path, 606 partials_dir=partials_dir)) 607 608 partials_cmd = list() 609 for i, partial in enumerate(partials): 610 dep_subdir = job_params[i][1] 611 if dep_subdir in dep_subdir_overlay_set: 612 partials_cmd += ['-R'] 613 partials_cmd += [partial] 614 return partials_cmd 615 616 617def _CreateResourceInfoFile(path_info, info_path, dependencies_res_zips): 618 for zip_file in dependencies_res_zips: 619 zip_info_file_path = zip_file + '.info' 620 if os.path.exists(zip_info_file_path): 621 path_info.MergeInfoFile(zip_info_file_path) 622 path_info.Write(info_path) 623 624 625def _RemoveUnwantedLocalizedStrings(dep_subdirs, options): 626 """Remove localized strings that should not go into the final output. 627 628 Args: 629 dep_subdirs: List of resource dependency directories. 630 options: Command-line options namespace. 631 """ 632 # Collect locale and file paths from the existing subdirs. 633 # The following variable maps Android locale names to 634 # sets of corresponding xml file paths. 635 locale_to_files_map = collections.defaultdict(set) 636 for directory in dep_subdirs: 637 for f in _IterFiles(directory): 638 locale = resource_utils.FindLocaleInStringResourceFilePath(f) 639 if locale: 640 locale_to_files_map[locale].add(f) 641 642 all_locales = set(locale_to_files_map) 643 644 # Set A: wanted locales, either all of them or the 645 # list provided by --locale-allowlist. 646 wanted_locales = all_locales 647 if options.locale_allowlist: 648 wanted_locales = _ToAndroidLocales(options.locale_allowlist) 649 650 # Set B: shared resources locales, which is either set A 651 # or the list provided by --shared-resources-allowlist-locales 652 shared_resources_locales = wanted_locales 653 shared_names_allowlist = set() 654 if options.shared_resources_allowlist_locales: 655 shared_names_allowlist = set( 656 resource_utils.GetRTxtStringResourceNames( 657 options.shared_resources_allowlist)) 658 659 shared_resources_locales = _ToAndroidLocales( 660 options.shared_resources_allowlist_locales) 661 662 # Remove any file that belongs to a locale not covered by 663 # either A or B. 664 removable_locales = (all_locales - wanted_locales - shared_resources_locales) 665 for locale in removable_locales: 666 for path in locale_to_files_map[locale]: 667 os.remove(path) 668 669 # For any locale in B but not in A, only keep the shared 670 # resource strings in each file. 671 for locale in shared_resources_locales - wanted_locales: 672 for path in locale_to_files_map[locale]: 673 resource_utils.FilterAndroidResourceStringsXml( 674 path, lambda x: x in shared_names_allowlist) 675 676 # For any locale in A but not in B, only keep the strings 677 # that are _not_ from shared resources in the file. 678 for locale in wanted_locales - shared_resources_locales: 679 for path in locale_to_files_map[locale]: 680 resource_utils.FilterAndroidResourceStringsXml( 681 path, lambda x: x not in shared_names_allowlist) 682 683 684def _FilterResourceFiles(dep_subdirs, keep_predicate): 685 # Create a function that selects which resource files should be packaged 686 # into the final output. Any file that does not pass the predicate will 687 # be removed below. 688 png_paths = [] 689 for directory in dep_subdirs: 690 for f in _IterFiles(directory): 691 if not keep_predicate(f): 692 os.remove(f) 693 elif f.endswith('.png'): 694 png_paths.append(f) 695 696 return png_paths 697 698 699def _PackageApk(options, build): 700 """Compile and link resources with aapt2. 701 702 Args: 703 options: The command-line options. 704 build: BuildContext object. 705 Returns: 706 The manifest package name for the APK. 707 """ 708 logging.debug('Extracting resource .zips') 709 dep_subdirs = [] 710 dep_subdir_overlay_set = set() 711 for dependency_res_zip in options.dependencies_res_zips: 712 extracted_dep_subdirs = resource_utils.ExtractDeps([dependency_res_zip], 713 build.deps_dir) 714 dep_subdirs += extracted_dep_subdirs 715 if dependency_res_zip in options.dependencies_res_zip_overlays: 716 dep_subdir_overlay_set.update(extracted_dep_subdirs) 717 718 logging.debug('Applying locale transformations') 719 path_info = resource_utils.ResourceInfoFile() 720 _RenameLocaleResourceDirs(dep_subdirs, path_info) 721 722 logging.debug('Applying file-based exclusions') 723 keep_predicate = _CreateKeepPredicate(options.resource_exclusion_regex, 724 options.resource_exclusion_exceptions) 725 png_paths = _FilterResourceFiles(dep_subdirs, keep_predicate) 726 727 if options.locale_allowlist or options.shared_resources_allowlist_locales: 728 logging.debug('Applying locale-based string exclusions') 729 _RemoveUnwantedLocalizedStrings(dep_subdirs, options) 730 731 if png_paths and options.png_to_webp: 732 logging.debug('Converting png->webp') 733 _ConvertToWebP(options.webp_binary, png_paths, path_info, 734 options.webp_cache_dir) 735 logging.debug('Applying drawable transformations') 736 for directory in dep_subdirs: 737 _MoveImagesToNonMdpiFolders(directory, path_info) 738 _RemoveImageExtensions(directory, path_info) 739 740 logging.debug('Running aapt2 compile') 741 exclusion_rules = [x.split(':', 1) for x in options.values_filter_rules] 742 partials = _CompileDeps(options.aapt2_path, dep_subdirs, 743 dep_subdir_overlay_set, build.temp_dir, 744 exclusion_rules) 745 746 link_command = [ 747 options.aapt2_path, 748 'link', 749 '--auto-add-overlay', 750 '--no-version-vectors', 751 '--no-xml-namespaces', 752 '--output-text-symbols', 753 build.r_txt_path, 754 ] 755 756 for j in options.include_resources: 757 link_command += ['-I', j] 758 if options.proguard_file: 759 link_command += ['--proguard', build.proguard_path] 760 link_command += ['--proguard-minimal-keep-rules'] 761 if options.emit_ids_out: 762 link_command += ['--emit-ids', build.emit_ids_path] 763 764 # Note: only one of --proto-format, --shared-lib or --app-as-shared-lib 765 # can be used with recent versions of aapt2. 766 if options.shared_resources: 767 link_command.append('--shared-lib') 768 769 if options.package_id: 770 link_command += [ 771 '--package-id', 772 '0x%02x' % options.package_id, 773 '--allow-reserved-package-id', 774 ] 775 776 fixed_manifest, desired_manifest_package_name, fixed_manifest_package = ( 777 _FixManifest(options, build.temp_dir)) 778 if options.rename_manifest_package: 779 desired_manifest_package_name = options.rename_manifest_package 780 781 link_command += [ 782 '--manifest', fixed_manifest, '--rename-manifest-package', 783 desired_manifest_package_name 784 ] 785 786 if options.package_id is not None: 787 package_id = options.package_id 788 elif options.shared_resources: 789 package_id = 0 790 else: 791 package_id = 0x7f 792 _CreateStableIdsFile(options.use_resource_ids_path, build.stable_ids_path, 793 fixed_manifest_package, package_id) 794 link_command += ['--stable-ids', build.stable_ids_path] 795 796 link_command += partials 797 798 # We always create a binary arsc file first, then convert to proto, so flags 799 # such as --shared-lib can be supported. 800 link_command += ['-o', build.arsc_path] 801 802 logging.debug('Starting: aapt2 link') 803 link_proc = subprocess.Popen(link_command) 804 805 # Create .res.info file in parallel. 806 if options.info_path: 807 logging.debug('Creating .res.info file') 808 _CreateResourceInfoFile(path_info, build.info_path, 809 options.dependencies_res_zips) 810 811 exit_code = link_proc.wait() 812 assert exit_code == 0, f'aapt2 link cmd failed with {exit_code=}' 813 logging.debug('Finished: aapt2 link') 814 815 if options.shared_resources: 816 logging.debug('Resolving styleables in R.txt') 817 # Need to resolve references because unused resource removal tool does not 818 # support references in R.txt files. 819 resource_utils.ResolveStyleableReferences(build.r_txt_path) 820 821 if exit_code: 822 raise subprocess.CalledProcessError(exit_code, link_command) 823 824 if options.proguard_file and (options.shared_resources 825 or options.app_as_shared_lib): 826 # Make sure the R class associated with the manifest package does not have 827 # its onResourcesLoaded method obfuscated or removed, so that the framework 828 # can call it in the case where the APK is being loaded as a library. 829 with open(build.proguard_path, 'a') as proguard_file: 830 keep_rule = ''' 831 -keep,allowoptimization class {package}.R {{ 832 public static void onResourcesLoaded(int); 833 }} 834 '''.format(package=desired_manifest_package_name) 835 proguard_file.write(textwrap.dedent(keep_rule)) 836 837 logging.debug('Running aapt2 convert') 838 build_utils.CheckOutput([ 839 options.aapt2_path, 'convert', '--output-format', 'proto', '-o', 840 build.proto_path, build.arsc_path 841 ]) 842 843 # Workaround for b/147674078. This is only needed for WebLayer and does not 844 # affect WebView usage, since WebView does not used dynamic attributes. 845 if options.shared_resources: 846 logging.debug('Hardcoding dynamic attributes') 847 protoresources.HardcodeSharedLibraryDynamicAttributes( 848 build.proto_path, options.is_bundle_module, 849 options.shared_resources_allowlist) 850 851 build_utils.CheckOutput([ 852 options.aapt2_path, 'convert', '--output-format', 'binary', '-o', 853 build.arsc_path, build.proto_path 854 ]) 855 856 # Sanity check that the created resources have the expected package ID. 857 logging.debug('Performing sanity check') 858 _, actual_package_id = resource_utils.ExtractArscPackage( 859 options.aapt2_path, 860 build.arsc_path if options.arsc_path else build.proto_path) 861 # When there are no resources, ExtractArscPackage returns (None, None), in 862 # this case there is no need to check for matching package ID. 863 if actual_package_id is not None and actual_package_id != package_id: 864 raise Exception('Invalid package ID 0x%x (expected 0x%x)' % 865 (actual_package_id, package_id)) 866 867 return desired_manifest_package_name 868 869 870def _CreateStableIdsFile(in_path, out_path, package_name, package_id): 871 """Transforms a file generated by --emit-ids from another package. 872 873 --stable-ids is generally meant to be used by different versions of the same 874 package. To make it work for other packages, we need to transform the package 875 name references to match the package that resources are being generated for. 876 """ 877 if in_path: 878 data = pathlib.Path(in_path).read_text() 879 else: 880 # Force IDs to use 0x01 for the type byte in order to ensure they are 881 # different from IDs generated by other apps. https://crbug.com/1293336 882 data = 'pkg:id/fake_resource_id = 0x7f010000\n' 883 # Replace "pkg:" with correct package name. 884 data = re.sub(r'^.*?:', package_name + ':', data, flags=re.MULTILINE) 885 # Replace "0x7f" with correct package id. 886 data = re.sub(r'0x..', '0x%02x' % package_id, data) 887 pathlib.Path(out_path).write_text(data) 888 889 890def _WriteOutputs(options, build): 891 possible_outputs = [ 892 (options.srcjar_out, build.srcjar_path), 893 (options.r_text_out, build.r_txt_path), 894 (options.arsc_path, build.arsc_path), 895 (options.proto_path, build.proto_path), 896 (options.proguard_file, build.proguard_path), 897 (options.emit_ids_out, build.emit_ids_path), 898 (options.info_path, build.info_path), 899 ] 900 901 for final, temp in possible_outputs: 902 # Write file only if it's changed. 903 if final and not (os.path.exists(final) and filecmp.cmp(final, temp)): 904 shutil.move(temp, final) 905 906 907def _CreateNormalizedManifestForVerification(options): 908 with build_utils.TempDir() as tempdir: 909 fixed_manifest, _, _ = _FixManifest(options, tempdir) 910 with open(fixed_manifest) as f: 911 return manifest_utils.NormalizeManifest( 912 f.read(), options.verification_version_code_offset, 913 options.verification_library_version_offset) 914 915 916def main(args): 917 build_utils.InitLogging('RESOURCE_DEBUG') 918 args = build_utils.ExpandFileArgs(args) 919 options = _ParseArgs(args) 920 921 if options.expected_file: 922 actual_data = _CreateNormalizedManifestForVerification(options) 923 diff_utils.CheckExpectations(actual_data, options) 924 if options.only_verify_expectations: 925 return 926 927 path = options.arsc_path or options.proto_path 928 debug_temp_resources_dir = os.environ.get('TEMP_RESOURCES_DIR') 929 if debug_temp_resources_dir: 930 path = os.path.join(debug_temp_resources_dir, os.path.basename(path)) 931 else: 932 # Use a deterministic temp directory since .pb files embed the absolute 933 # path of resources: crbug.com/939984 934 path = path + '.tmpdir' 935 build_utils.DeleteDirectory(path) 936 937 with resource_utils.BuildContext( 938 temp_dir=path, keep_files=bool(debug_temp_resources_dir)) as build: 939 940 manifest_package_name = _PackageApk(options, build) 941 942 # If --shared-resources-allowlist is used, all the resources listed in the 943 # corresponding R.txt file will be non-final, and an onResourcesLoaded() 944 # will be generated to adjust them at runtime. 945 # 946 # Otherwise, if --shared-resources is used, the all resources will be 947 # non-final, and an onResourcesLoaded() method will be generated too. 948 # 949 # Otherwise, all resources will be final, and no method will be generated. 950 # 951 rjava_build_options = resource_utils.RJavaBuildOptions() 952 if options.shared_resources_allowlist: 953 rjava_build_options.ExportSomeResources( 954 options.shared_resources_allowlist) 955 rjava_build_options.GenerateOnResourcesLoaded() 956 if options.shared_resources: 957 # The final resources will only be used in WebLayer, so hardcode the 958 # package ID to be what WebLayer expects. 959 rjava_build_options.SetFinalPackageId( 960 protoresources.SHARED_LIBRARY_HARDCODED_ID) 961 elif options.shared_resources or options.app_as_shared_lib: 962 rjava_build_options.ExportAllResources() 963 rjava_build_options.GenerateOnResourcesLoaded() 964 965 custom_root_package_name = options.r_java_root_package_name 966 grandparent_custom_package_name = None 967 968 # Always generate an R.java file for the package listed in 969 # AndroidManifest.xml because this is where Android framework looks to find 970 # onResourcesLoaded() for shared library apks. While not actually necessary 971 # for application apks, it also doesn't hurt. 972 apk_package_name = manifest_package_name 973 974 if options.package_name and not options.arsc_package_name: 975 # Feature modules have their own custom root package name and should 976 # inherit from the appropriate base module package. This behaviour should 977 # not be present for test apks with an apk under test. Thus, 978 # arsc_package_name is used as it is only defined for test apks with an 979 # apk under test. 980 custom_root_package_name = options.package_name 981 grandparent_custom_package_name = options.r_java_root_package_name 982 # Feature modules have the same manifest package as the base module but 983 # they should not create an R.java for said manifest package because it 984 # will be created in the base module. 985 apk_package_name = None 986 987 if options.srcjar_out: 988 logging.debug('Creating R.srcjar') 989 resource_utils.CreateRJavaFiles(build.srcjar_dir, apk_package_name, 990 build.r_txt_path, 991 options.extra_res_packages, 992 rjava_build_options, options.srcjar_out, 993 custom_root_package_name, 994 grandparent_custom_package_name) 995 with action_helpers.atomic_output(build.srcjar_path) as f: 996 zip_helpers.zip_directory(f, build.srcjar_dir) 997 998 logging.debug('Copying outputs') 999 _WriteOutputs(options, build) 1000 1001 if options.depfile: 1002 assert options.srcjar_out, 'Update first output below and remove assert.' 1003 depfile_deps = (options.dependencies_res_zips + 1004 options.dependencies_res_zip_overlays + 1005 options.include_resources) 1006 action_helpers.write_depfile(options.depfile, options.srcjar_out, 1007 depfile_deps) 1008 1009 1010if __name__ == '__main__': 1011 main(sys.argv[1:]) 1012