1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195

build / android / pylib / utils / app_bundle_utils.py [blame]

# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import json
import logging
import os
import pathlib
import re
import shutil
import sys
import zipfile

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'gyp'))

from util import build_utils
from util import md5_check
from util import resource_utils
import bundletool

# "system_apks" is "default", but with locale list and compressed dex.
_SYSTEM_MODES = ('system', 'system_apks')
BUILD_APKS_MODES = _SYSTEM_MODES + ('default', 'universal')
OPTIMIZE_FOR_OPTIONS = ('ABI', 'SCREEN_DENSITY', 'LANGUAGE',
                        'TEXTURE_COMPRESSION_FORMAT')

_ALL_ABIS = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']


def _BundleMinSdkVersion(bundle_path):
  manifest_data = bundletool.RunBundleTool(
      ['dump', 'manifest', '--bundle', bundle_path])
  return int(re.search(r'minSdkVersion.*?(\d+)', manifest_data).group(1))


def _CreateDeviceSpec(bundle_path, sdk_version, locales):
  if not sdk_version:
    sdk_version = _BundleMinSdkVersion(bundle_path)

  # Setting sdkVersion=minSdkVersion prevents multiple per-minSdkVersion .apk
  # files from being created within the .apks file.
  return {
      'screenDensity': 1000,  # Ignored since we don't split on density.
      'sdkVersion': sdk_version,
      'supportedAbis': _ALL_ABIS,  # Our .aab files are already split on abi.
      'supportedLocales': locales,
  }


def _FixBundleDexCompressionGlob(src_bundle, dst_bundle):
  # Modifies the BundleConfig.pb of the given .aab to add "classes*.dex" to the
  # "uncompressedGlob" list.
  with zipfile.ZipFile(src_bundle) as src, \
      zipfile.ZipFile(dst_bundle, 'w') as dst:
    for info in src.infolist():
      data = src.read(info)
      if info.filename == 'BundleConfig.pb':
        # A classesX.dex entry is added by create_app_bundle.py so that we can
        # modify it here in order to have it take effect. b/176198991
        data = data.replace(b'classesX.dex', b'classes*.dex')
      dst.writestr(info, data)


def GenerateBundleApks(bundle_path,
                       bundle_apks_path,
                       aapt2_path,
                       keystore_path,
                       keystore_password,
                       keystore_alias,
                       mode=None,
                       local_testing=False,
                       minimal=False,
                       minimal_sdk_version=None,
                       check_for_noop=True,
                       system_image_locales=None,
                       optimize_for=None):
  """Generate an .apks archive from a an app bundle if needed.

  Args:
    bundle_path: Input bundle file path.
    bundle_apks_path: Output bundle .apks archive path. Name must end with
      '.apks' or this operation will fail.
    aapt2_path: Path to aapt2 build tool.
    keystore_path: Path to keystore.
    keystore_password: Keystore password, as a string.
    keystore_alias: Keystore signing key alias.
    mode: Build mode, which must be either None or one of BUILD_APKS_MODES.
    minimal: Create the minimal set of apks possible (english-only).
    minimal_sdk_version: Use this sdkVersion when |minimal| or
      |system_image_locales| args are present.
    check_for_noop: Use md5_check to short-circuit when inputs have not changed.
    system_image_locales: Locales to package in the APK when mode is "system"
      or "system_compressed".
    optimize_for: Overrides split configuration, which must be None or
      one of OPTIMIZE_FOR_OPTIONS.
  """
  device_spec = None
  if minimal_sdk_version:
    assert minimal or system_image_locales, (
        'minimal_sdk_version is only used when minimal or system_image_locales '
        'is specified')
  if minimal:
    # Measure with one language split installed. Use Hindi because it is
    # popular. resource_size.py looks for splits/base-hi.apk.
    # Note: English is always included since it's in base-master.apk.
    device_spec = _CreateDeviceSpec(bundle_path, minimal_sdk_version, ['hi'])
  elif mode in _SYSTEM_MODES:
    if not system_image_locales:
      raise Exception('system modes require system_image_locales')
    # Bundletool doesn't seem to understand device specs with locales in the
    # form of "<lang>-r<region>", so just provide the language code instead.
    locales = [
        resource_utils.ToAndroidLocaleName(l).split('-')[0]
        for l in system_image_locales
    ]
    device_spec = _CreateDeviceSpec(bundle_path, minimal_sdk_version, locales)

  def rebuild():
    logging.info('Building %s', bundle_apks_path)
    with build_utils.TempDir() as tmp_dir:
      tmp_apks_file = os.path.join(tmp_dir, 'output.apks')
      cmd_args = [
          'build-apks',
          '--aapt2=%s' % aapt2_path,
          '--output=%s' % tmp_apks_file,
          '--ks=%s' % keystore_path,
          '--ks-pass=pass:%s' % keystore_password,
          '--ks-key-alias=%s' % keystore_alias,
          '--overwrite',
      ]
      input_bundle_path = bundle_path
      # Work around bundletool not respecting uncompressDexFiles setting.
      # b/176198991
      if mode not in _SYSTEM_MODES and _BundleMinSdkVersion(bundle_path) >= 27:
        input_bundle_path = os.path.join(tmp_dir, 'system.aab')
        _FixBundleDexCompressionGlob(bundle_path, input_bundle_path)

      cmd_args += ['--bundle=%s' % input_bundle_path]

      if local_testing:
        cmd_args += ['--local-testing']

      if mode is not None:
        if mode not in BUILD_APKS_MODES:
          raise Exception('Invalid mode parameter %s (should be in %s)' %
                          (mode, BUILD_APKS_MODES))
        if mode != 'system_apks':
          cmd_args += ['--mode=' + mode]
        else:
          # Specify --optimize-for to prevent language splits being created.
          cmd_args += ['--optimize-for=device_tier']

      if optimize_for:
        if optimize_for not in OPTIMIZE_FOR_OPTIONS:
          raise Exception('Invalid optimize_for parameter %s '
                          '(should be in %s)' %
                          (mode, OPTIMIZE_FOR_OPTIONS))
        cmd_args += ['--optimize-for=' + optimize_for]

      if device_spec:
        data = json.dumps(device_spec)
        logging.debug('Device Spec: %s', data)
        spec_file = pathlib.Path(tmp_dir) / 'device.json'
        spec_file.write_text(data)
        cmd_args += ['--device-spec=' + str(spec_file)]

      bundletool.RunBundleTool(cmd_args)

      shutil.move(tmp_apks_file, bundle_apks_path)

  if check_for_noop:
    input_paths = [
        bundle_path,
        bundletool.BUNDLETOOL_JAR_PATH,
        aapt2_path,
        keystore_path,
    ]
    input_strings = [
        keystore_password,
        keystore_alias,
        device_spec,
    ]
    if mode is not None:
      input_strings.append(mode)

    # Avoid rebuilding (saves ~20s) when the input files have not changed. This
    # is essential when calling the apk_operations.py script multiple times with
    # the same bundle (e.g. out/Debug/bin/monochrome_public_bundle run).
    md5_check.CallAndRecordIfStale(
        rebuild,
        input_paths=input_paths,
        input_strings=input_strings,
        output_paths=[bundle_apks_path])
  else:
    rebuild()