Commit 2cfbe6ab authored by David 'Digit' Turner's avatar David 'Digit' Turner Committed by Commit Bot

android: Create app bundle wrapper script for bundle targets.

This ensure that the GN android_app_bundle() template will also
generate a wrapper script to manage bundles easily from the local
machine during development and testing.

For example, $OUT/bin/chrome_public_bundle is generated for the
'chrome_public_bundle' target.

The script is very similar to the wrapper script for APKs, it
also uses apk_operations.py, which has been lightly modified
to support bundles.

A few important notes about this CL:

- The 'generate_apks' variable of android_app_bundle() is now
  a no-op, i.e. .apks archives are no longer generated by the
  build itself. Instead, the wrapper script will generate the
  .apks archive on demand.

  This simplifies the build, and allows to locally test any
  bundle if needed easily.

  The variable still exists to avoid breaking the clank/ build.

- The generation of .apks archives from bundles is implemented
  by using 'bundletool build-apks', which is called from the
  new helper script app_bundle_utils.py

  This is performed lazily through a small closure function
  generated by the bundle wrapper script, which is passed
  to the new apk_operations.RunForBundle() function.

  This scheme is used to considerably reduce the amount of
  modifications required in apk_operations.py, since this
  avoids the need to pass 6 more command-line flags down to
  the internal _Command class.

- The new 'build-bundle-apks' command can be used to build
  said .apks archive on demand, and optionally copy it to a
  different location.

  Generation is also triggered in case of 'install'.

  MD5 checks are used to ensure the .apks archive is only
  rebuild if strictly necessary, since this is a slow
  process (e.g. around 40s for chrome_public_bundle on my
  machine).

- Installation currently uses 'bundletool install-apks'
  directly and avoids using devil, since the latter doesn't
  have a good way to support bundle installation.

  Proper support for app bundles in devil will be added
  in a future CL, after which apk_operations.py may be
  updated to use it.

What was tested and works:

  'devices', 'install', 'uninstall', 'launch' (without a URL),
  'stop', 'clear-data', 'logcat', 'ps', 'disk-usage', 'run'

Tested but does not work:

  'launch <URL>' (but see TODO inside apk_operations.py)

Not tested yet:

  'argv', 'gdb', 'mem-usage', 'shell', 'compile-dex',
  'profile'

BUG=862525
R=agrieve@chromium.org, benmason@chromium.org, jbudorick@chromium.org

Change-Id: Ic1074a723b0a705f1f53ff484a19a1ca1041e7d7
Reviewed-on: https://chromium-review.googlesource.com/1146811
Commit-Queue: David Turner <digit@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577842}
parent 061a338c
This diff is collapsed.
#!/usr/bin/env python
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Create a wrapper script to manage an Android App Bundle."""
import argparse
import os
import string
import sys
from util import build_utils
SCRIPT_TEMPLATE = string.Template("""\
#!/usr/bin/env python
#
# This file was generated by build/android/gyp/create_bundle_wrapper_script.py
import os
import sys
def main():
script_directory = os.path.dirname(__file__)
resolve = lambda p: p if p is None else os.path.abspath(os.path.join(
script_directory, p))
sys.path.append(resolve(${WRAPPED_SCRIPT_DIR}))
import apk_operations
apk_operations.RunForBundle(output_directory=resolve(${OUTPUT_DIR}),
bundle_path=resolve(${BUNDLE_PATH}),
bundle_apks_path=resolve(${BUNDLE_APKS_PATH}),
aapt2_path=resolve(${AAPT2_PATH}),
keystore_path=resolve(${KEYSTORE_PATH}),
keystore_password=${KEYSTORE_PASSWORD},
keystore_alias=${KEY_NAME},
package_name=${PACKAGE_NAME},
command_line_flags_file=${FLAGS_FILE},
proguard_mapping_path=resolve(${MAPPING_PATH}),
target_cpu=${TARGET_CPU})
if __name__ == '__main__':
sys.exit(main())
""")
def main(args):
args = build_utils.ExpandFileArgs(args)
parser = argparse.ArgumentParser()
parser.add_argument('--script-output-path', required=True,
help='Output path for executable script.')
parser.add_argument('--bundle-path', required=True)
parser.add_argument('--bundle-apks-path', required=True)
parser.add_argument('--package-name', required=True)
parser.add_argument('--aapt2-path', required=True)
parser.add_argument('--keystore-path', required=True)
parser.add_argument('--keystore-password', required=True)
parser.add_argument('--key-name', required=True)
parser.add_argument('--command-line-flags-file')
parser.add_argument('--proguard-mapping-path')
parser.add_argument('--target-cpu')
args = parser.parse_args(args)
def relativize(path):
"""Returns the path relative to the output script directory."""
if path is None:
return path
return os.path.relpath(path, os.path.dirname(args.script_output_path))
wrapped_script_dir = os.path.join(os.path.dirname(__file__), os.path.pardir)
wrapped_script_dir = relativize(wrapped_script_dir)
with open(args.script_output_path, 'w') as script:
script_dict = {
'WRAPPED_SCRIPT_DIR': repr(wrapped_script_dir),
'OUTPUT_DIR': repr(relativize('.')),
'BUNDLE_PATH': repr(relativize(args.bundle_path)),
'BUNDLE_APKS_PATH': repr(relativize(args.bundle_apks_path)),
'PACKAGE_NAME': repr(args.package_name),
'AAPT2_PATH': repr(relativize(args.aapt2_path)),
'KEYSTORE_PATH': repr(relativize(args.keystore_path)),
'KEYSTORE_PASSWORD': repr(args.keystore_password),
'KEY_NAME': repr(args.key_name),
'MAPPING_PATH': repr(relativize(args.proguard_mapping_path)),
'FLAGS_FILE': repr(args.command_line_flags_file),
'TARGET_CPU': repr(args.target_cpu),
}
script.write(SCRIPT_TEMPLATE.substitute(script_dict))
os.chmod(args.script_output_path, 0750)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'gyp'))
from util import build_utils
from util import md5_check
import bundletool
def GenerateBundleApks(bundle_path, bundle_apks_path, aapt2_path,
keystore_path, keystore_password, keystore_alias):
"""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.
"""
# NOTE: BUNDLETOOL_JAR_PATH is added to input_strings, rather than
# input_paths, to speed up MD5 computations by about 400ms (the .jar file
# contains thousands of class files which are checked independently,
# resulting in an .md5.stamp of more than 60000 lines!).
input_paths = [
bundle_path,
aapt2_path,
keystore_path
]
input_strings = [
keystore_password,
keystore_alias,
bundletool.BUNDLETOOL_JAR_PATH,
# NOTE: BUNDLETOOL_VERSION is already part of BUNDLETOOL_JAR_PATH, but
# it's simpler to assume that this may not be the case in the future.
bundletool.BUNDLETOOL_VERSION
]
output_paths = [bundle_apks_path]
def rebuild():
logging.info('Building %s', os.path.basename(bundle_apks_path))
with build_utils.AtomicOutput(bundle_apks_path) as tmp_apks:
cmd_args = [
'java', '-jar', bundletool.BUNDLETOOL_JAR_PATH, 'build-apks',
'--aapt2=%s' % aapt2_path,
'--output=%s' % tmp_apks.name,
'--bundle=%s' % bundle_path,
'--ks=%s' % keystore_path,
'--ks-pass=pass:%s' % keystore_password,
'--ks-key-alias=%s' % keystore_alias,
'--overwrite',
]
build_utils.CheckOutput(cmd_args)
md5_check.CallAndRecordIfStale(
rebuild,
input_paths=input_paths,
input_strings=input_strings,
output_paths=output_paths)
......@@ -3600,8 +3600,7 @@ if (enable_java_templates) {
# directory. Defaults to "$root_build_dir/apks".
#
# bundle_name: Optional. If set, the bundle will be output to the
# filename "${bundle_name}.aab". Also, if generate_apks is true
# "${bundle_name}.apks" will be used. Defaults to "${target_name}".
# filename "${bundle_name}.aab".
#
# extra_modules: Optional list of scopes, one per extra module used by
# this bundle. Each scope must have a 'name' field that specifies the
......@@ -3612,13 +3611,6 @@ if (enable_java_templates) {
# extra_module_apk_targets: Optional list of android_apk target matching
# the names listed in extra_module_names.
#
# generate_apks: Optional. If true, generate an .apks archive that
# contains all generated APK splits from the bundle. These are
# installable to a local device using the bundletool 'install-apks'
# command.
#
# NOTE: This creates a new target with the name "${target_name}_apks"
#
# enable_language_splits: Optional. If true, enable APK splits based
# on languages.
#
......@@ -3632,6 +3624,13 @@ if (enable_java_templates) {
# keystore_password: optional keystore password, used only when
# generating APKs.
#
# command_line_flags_file: Optional. If provided, named of the on-device
# file that will be used to store command-line arguments. The default
# is 'command_line_flags_file', but this is typically redefined to
# something more specific for certain bundles (e.g. the Chromium based
# APKs use 'chrome-command-line', the WebView one uses
# 'webview-command-line').
#
# Example:
# android_app_bundle("chrome_public_bundle") {
# base_module_target = "//chrome/android:chrome_public_apk"
......@@ -3733,7 +3732,6 @@ if (enable_java_templates) {
_rebased_bundle_path = rebase_path(_bundle_path, root_build_dir)
_sign_bundle = defined(invoker.sign_bundle) && invoker.sign_bundle
_generate_apks = defined(invoker.generate_apks) && invoker.generate_apks
_split_dimensions = []
if (defined(invoker.enable_language_splits) &&
......@@ -3741,25 +3739,23 @@ if (enable_java_templates) {
_split_dimensions += [ "language" ]
}
if (_sign_bundle || _generate_apks) {
_keystore_path = android_keystore_path
_keystore_password = android_keystore_password
_keystore_name = android_keystore_name
_keystore_path = android_keystore_path
_keystore_password = android_keystore_password
_keystore_name = android_keystore_name
if (defined(invoker.keystore_path)) {
_keystore_path = invoker.keystore_path
_keystore_password = invoker.keystore_password
_keystore_name = invoker.keystore_name
}
if (defined(invoker.keystore_path)) {
_keystore_path = invoker.keystore_path
_keystore_password = invoker.keystore_password
_keystore_name = invoker.keystore_name
}
_rebased_keystore_path = rebase_path(_keystore_path, root_build_dir)
_rebased_keystore_path = rebase_path(_keystore_path, root_build_dir)
if (_sign_bundle) {
# For now, the same keys are used to sign the bundle and the set of
# generated APKs. In the future, signing the bundle may require a
# different set of keys.
_bundle_keystore_name = _keystore_name
}
if (_sign_bundle) {
# For now, the same keys are used to sign the bundle and the set of
# generated APKs. In the future, signing the bundle may require a
# different set of keys.
_bundle_keystore_name = _keystore_name
}
# NOTE: Keep this consistent with the imports of create_app_bundle.py
......@@ -3769,7 +3765,7 @@ if (enable_java_templates) {
"//build/android/gyp/bundletool.py",
]
_bundle_target_name = target_name
_bundle_target_name = "${target_name}__bundle"
action(_bundle_target_name) {
script = "//build/android/gyp/create_app_bundle.py"
inputs = _all_module_zip_paths + _all_module_build_configs +
......@@ -3806,45 +3802,65 @@ if (enable_java_templates) {
}
}
if (_generate_apks) {
_android_aapt2_path = android_sdk_tools_bundle_aapt2
# Generate a wrapper script for the bundle.
_android_aapt2_path = android_sdk_tools_bundle_aapt2
_bundle_apks_target = "${target_name}_apks"
_bundle_apks_path = "$_bundle_base_path/$_bundle_name.apks"
_bundle_apks_path = "$_bundle_base_path/$_bundle_name.apks"
_bundle_wrapper_script_dir = "$root_build_dir/bin"
_bundle_wrapper_script_path = "$_bundle_wrapper_script_dir/$target_name"
# NOTE: Keep this consistent with the imports of app_bundle_to_apks.py
# note that resource_utils.py imports build_utils.py
_app_bundle_to_apks_py_imports =
build_utils_py + [ "//build/android/gyp/bundletool.py" ]
_base_module_build_config = _all_module_build_configs[0]
_base_module_build_config_target =
"${invoker.base_module_target}__build_config"
_rebased_base_module_build_config =
rebase_path(_base_module_build_config, root_build_dir)
action(_bundle_apks_target) {
script = "//build/android/gyp/app_bundle_to_apks.py"
inputs = _app_bundle_to_apks_py_imports + [
_bundle_path,
_android_aapt2_path,
_keystore_path,
]
outputs = [
_bundle_apks_path,
]
deps = [
":$_bundle_target_name",
]
args = [
"--aapt2",
rebase_path(_android_aapt2_path, root_build_dir),
"--bundle",
_rebased_bundle_path,
"--out-zip",
rebase_path(_bundle_apks_path, root_build_dir),
"--keystore-path",
_rebased_keystore_path,
"--keystore-password",
_keystore_password,
"--key-name",
_keystore_name,
action("${target_name}__wrapper_script") {
script = "//build/android/gyp/create_bundle_wrapper_script.py"
inputs = build_utils_py + [
"//build/android/gyp/bundletool.py",
_base_module_build_config,
]
outputs = [
_bundle_wrapper_script_path,
]
deps = [
_base_module_build_config_target,
]
args = [
"--script-output-path",
rebase_path(_bundle_wrapper_script_path, root_build_dir),
"--package-name=@FileArg(" +
"$_rebased_base_module_build_config:deps_info:package_name)",
"--aapt2",
rebase_path(_android_aapt2_path, root_build_dir),
"--bundle-path",
_rebased_bundle_path,
"--bundle-apks-path",
rebase_path(_bundle_apks_path, root_build_dir),
"--target-cpu=$target_cpu",
"--keystore-path",
_rebased_keystore_path,
"--keystore-password",
_keystore_password,
"--key-name",
_keystore_name,
]
if (defined(invoker.command_line_flags_file)) {
args += [
"--command-line-flags-file",
invoker.command_line_flags_file,
]
}
# TODO(digit): Add --proguard-mapping-path argument.
}
group(target_name) {
deps = [
":${target_name}__bundle",
":${target_name}__wrapper_script",
]
}
}
}
......
......@@ -1602,10 +1602,6 @@ android_app_bundle("chrome_public_bundle") {
# Signing is very slow, only do that for official builds.
sign_bundle = is_official_build
# To ease local testing, also generate the .apks archive containing split
# APKs for this bundle.
generate_apks = true
enable_language_splits = true
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment