chromium/build/android/gyp/optimize_resources.py

#!/usr/bin/env python3
#
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import logging
import os
import sys

from util import build_utils
import action_helpers  # build_utils adds //build to sys.path.


def _ParseArgs(args):
  """Parses command line options.

  Returns:
    An options object as from argparse.ArgumentParser.parse_args()
  """
  parser = argparse.ArgumentParser()
  parser.add_argument('--aapt2-path',
                      required=True,
                      help='Path to the Android aapt2 tool.')
  parser.add_argument(
      '--short-resource-paths',
      action='store_true',
      help='Whether to shorten resource paths inside the apk or module.')
  parser.add_argument(
      '--strip-resource-names',
      action='store_true',
      help='Whether to strip resource names from the resource table of the apk '
      'or module.')
  parser.add_argument('--proto-path',
                      required=True,
                      help='Input proto format resources APK.')
  parser.add_argument('--resources-config-paths',
                      default='[]',
                      help='GN list of paths to aapt2 resources config files.')
  parser.add_argument('--r-text-in',
                      required=True,
                      help='Path to R.txt. Used to exclude id/ resources.')
  parser.add_argument(
      '--resources-path-map-out-path',
      help='Path to file produced by aapt2 that maps original resource paths '
      'to shortened resource paths inside the apk or module.')
  parser.add_argument('--optimized-proto-path',
                      required=True,
                      help='Output for `aapt2 optimize`.')
  options = parser.parse_args(args)

  options.resources_config_paths = action_helpers.parse_gn_list(
      options.resources_config_paths)

  if options.resources_path_map_out_path and not options.short_resource_paths:
    parser.error(
        '--resources-path-map-out-path requires --short-resource-paths')
  return options


def _CombineResourceConfigs(resources_config_paths, out_config_path):
  with open(out_config_path, 'w') as out_config:
    for config_path in resources_config_paths:
      with open(config_path) as config:
        out_config.write(config.read())
        out_config.write('\n')


def _ExtractNonCollapsableResources(rtxt_path):
  """Extract resources that should not be collapsed from the R.txt file

  Resources of type ID are references to UI elements/views. They are used by
  UI automation testing frameworks. They are kept in so that they don't break
  tests, even though they may not actually be used during runtime. See
  https://crbug.com/900993
  App icons (aka mipmaps) are sometimes referenced by other apps by name so must
  be keps as well. See https://b/161564466

  Args:
    rtxt_path: Path to R.txt file with all the resources
  Returns:
    List of resources in the form of <resource_type>/<resource_name>
  """
  resources = []
  _NO_COLLAPSE_TYPES = ['id', 'mipmap']
  with open(rtxt_path) as rtxt:
    for line in rtxt:
      for resource_type in _NO_COLLAPSE_TYPES:
        if ' {} '.format(resource_type) in line:
          resource_name = line.split()[2]
          resources.append('{}/{}'.format(resource_type, resource_name))
  return resources


def _OptimizeApk(output, options, temp_dir, unoptimized_path, r_txt_path):
  """Optimize intermediate .ap_ file with aapt2.

  Args:
    output: Path to write to.
    options: The command-line options.
    temp_dir: A temporary directory.
    unoptimized_path: path of the apk to optimize.
    r_txt_path: path to the R.txt file of the unoptimized apk.
  """
  optimize_command = [
      options.aapt2_path,
      'optimize',
      unoptimized_path,
      '-o',
      output,
  ]

  # Optimize the resources.pb file by obfuscating resource names and only
  # allow usage via R.java constant.
  if options.strip_resource_names:
    no_collapse_resources = _ExtractNonCollapsableResources(r_txt_path)
    gen_config_path = os.path.join(temp_dir, 'aapt2.config')
    if options.resources_config_paths:
      _CombineResourceConfigs(options.resources_config_paths, gen_config_path)
    with open(gen_config_path, 'a') as config:
      for resource in no_collapse_resources:
        config.write('{}#no_collapse\n'.format(resource))

    optimize_command += [
        '--collapse-resource-names',
        '--resources-config-path',
        gen_config_path,
    ]

  if options.short_resource_paths:
    optimize_command += ['--shorten-resource-paths']
  if options.resources_path_map_out_path:
    optimize_command += [
        '--resource-path-shortening-map', options.resources_path_map_out_path
    ]

  logging.debug('Running aapt2 optimize')
  build_utils.CheckOutput(optimize_command,
                          print_stdout=False,
                          print_stderr=False)


def main(args):
  options = _ParseArgs(args)
  with build_utils.TempDir() as temp_dir:
    _OptimizeApk(options.optimized_proto_path, options, temp_dir,
                 options.proto_path, options.r_text_in)


if __name__ == '__main__':
  main(sys.argv[1:])