#!/usr/bin/env python3
#
# Copyright (c) 2015 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.
"""Adds the code parts to a resource APK."""
import argparse
import logging
import os
import shutil
import sys
import tempfile
import zipfile
import zlib
import finalize_apk
from util
import build_utils
from util
import diff_utils
from util
import zipalign
# Input dex.jar files are zipaligned.
zipalign.ApplyZipFileZipAlignFix()
# Taken from aapt's Package.cpp:
_NO_COMPRESS_EXTENSIONS = (
'.jpg',
'.jpeg',
'.png',
'.gif',
'.wav',
'.mp2',
'.mp3',
'.ogg',
'.aac',
'.mpg',
'.mpeg',
'.mid',
'.midi',
'.smf',
'.jet',
'.rtttl',
'.imy',
'.xmf',
'.mp4',
'.m4a',
'.m4v',
'.3gp',
'.3gpp',
'.3g2',
'.3gpp2',
'.amr',
'.awb',
'.wma',
'.wmv',
'.webm')
def _ParseArgs(args):
parser = argparse.ArgumentParser()
build_utils.AddDepfileOption(parser)
parser.add_argument(
'--assets',
help=
'GYP-list of files to add as assets in the form '
'"srcPath:zipPath", where ":zipPath" is optional.')
parser.add_argument(
'--java-resources', help=
'GYP-list of java_resources JARs to include.')
parser.add_argument(
'--write-asset-list',
action=
'store_true',
help=
'Whether to create an assets/assets_list file.')
parser.add_argument(
'--uncompressed-assets',
help=
'Same as --assets, except disables compression.')
parser.add_argument(
'--resource-apk',
help=
'An .ap_ file built using aapt',
required=
True)
parser.add_argument(
'--output-apk',
help=
'Path to the output file',
required=
True)
parser.add_argument(
'--format', choices=[
'apk',
'bundle-module'],
default=
'apk', help=
'Specify output format.')
parser.add_argument(
'--dex-file',
help=
'Path to the classes.dex to use')
parser.add_argument(
'--jdk-libs-dex-file',
help=
'Path to classes.dex created by dex_jdk_libs.py')
parser.add_argument(
'--uncompress-dex', action=
'store_true',
help=
'Store .dex files uncompressed in the APK')
parser.add_argument(
'--native-libs',
action=
'append',
help=
'GYP-list of native libraries to include. '
'Can be specified multiple times.',
default=[])
parser.add_argument(
'--secondary-native-libs',
action=
'append',
help=
'GYP-list of native libraries for secondary '
'android-abi. Can be specified multiple times.',
default=[])
parser.add_argument(
'--android-abi',
help=
'Android architecture to use for native libraries')
parser.add_argument(
'--secondary-android-abi',
help=
'The secondary Android architecture to use for'
'secondary native libraries')
parser.add_argument(
'--is-multi-abi',
action=
'store_true',
help=
'Will add a placeholder for the missing ABI if no native libs or '
'placeholders are set for either the primary or secondary ABI. Can only '
'be set if both --android-abi and --secondary-android-abi are set.')
parser.add_argument(
'--native-lib-placeholders',
help=
'GYP-list of native library placeholders to add.')
parser.add_argument(
'--secondary-native-lib-placeholders',
help=
'GYP-list of native library placeholders to add '
'for the secondary ABI')
parser.add_argument(
'--uncompress-shared-libraries', default=
'False',
choices=[
'true',
'True',
'false',
'False'],
help=
'Whether to uncompress native shared libraries. Argument must be '
'a boolean value.')
parser.add_argument(
'--apksigner-jar', help=
'Path to the apksigner executable.')
parser.add_argument(
'--zipalign-path',
help=
'Path to the zipalign executable.')
parser.add_argument(
'--key-path',
help=
'Path to keystore for signing.')
parser.add_argument(
'--key-passwd',
help=
'Keystore password')
parser.add_argument(
'--key-name',
help=
'Keystore name')
parser.add_argument(
'--min-sdk-version', required=
True, help=
'Value of APK\'s minSdkVersion
')
parser.add_argument(
'--best-compression',
action=
'store_true',
help=
'Use zip -9 rather than zip -1')
parser.add_argument(
'--library-always-compress',
action=
'append',
help=
'The list of library files that we always compress.')
parser.add_argument(
'--library-renames',
action=
'append',
help=
'The list of library files that we prepend crazy. to their names.')
parser.add_argument(
'--warnings-as-errors',
action=
'store_true',
help=
'Treat all warnings as errors.')
diff_utils.AddCommandLineFlags(parser)
options = parser.parse_args(args)
options.assets = build_utils.ParseGnList(options.assets)
options.uncompressed_assets = build_utils.ParseGnList(
options.uncompressed_assets)
options.native_lib_placeholders = build_utils.ParseGnList(
options.native_lib_placeholders)
options.secondary_native_lib_placeholders = build_utils.ParseGnList(
options.secondary_native_lib_placeholders)
options.java_resources = build_utils.ParseGnList(options.java_resources)
options.native_libs = build_utils.ParseGnList(options.native_libs)
options.secondary_native_libs = build_utils.ParseGnList(
options.secondary_native_libs)
options.library_always_compress = build_utils.ParseGnList(
options.library_always_compress)
options.library_renames = build_utils.ParseGnList(options.library_renames)
# --apksigner-jar, --zipalign-path, --key-xxx arguments are
# required when building an APK, but not a bundle module.
if options.format ==
'apk':
required_args = [
'apksigner_jar',
'zipalign_path',
'key_path',
'key_passwd',
'key_name'
]
for required
in required_args:
if not vars(options)[required]:
raise Exception(
'Argument --%s is required for APKs.' % (
required.replace(
'_',
'-')))
options.uncompress_shared_libraries = \
options.uncompress_shared_libraries
in [
'true',
'True' ]
if not options.android_abi
and (options.native_libs
or
options.native_lib_placeholders):
raise Exception(
'Must specify --android-abi with --native-libs')
if not options.secondary_android_abi
and (options.secondary_native_libs
or
options.secondary_native_lib_placeholders):
raise Exception(
'Must specify --secondary-android-abi with'
' --secondary-native-libs')
if options.is_multi_abi
and not (options.android_abi
and options.secondary_android_abi):
raise Exception(
'Must specify --is-multi-abi with both --android-abi '
'and --secondary-android-abi.')
return options
def _SplitAssetPath(path):
"""Returns (src, dest) given an asset path in the form src[:dest]."""
path_parts = path.split(
':')
src_path = path_parts[0]
if len(path_parts) > 1:
dest_path = path_parts[1]
else:
dest_path = os.path.basename(src_path)
return src_path, dest_path
def _ExpandPaths(paths):
"""Converts src:dst into tuples and enumerates files within directories.
Args:
paths: Paths
in the form
"src_path:dest_path"
Returns:
A list of (src_path, dest_path) tuples sorted by dest_path (
for stable
ordering within output .apk).
"""
ret = []
for path
in paths:
src_path, dest_path = _SplitAssetPath(path)
if os.path.isdir(src_path):
for f
in build_utils.FindInDirectory(src_path,
'*'):
ret.append((f, os.path.join(dest_path, f[len(src_path) + 1:])))
else:
ret.append((src_path, dest_path))
ret.sort(key=
lambda t:t[1])
return ret
def _GetAssetsToAdd(path_tuples,
fast_align,
disable_compression=
False,
allow_reads=
True):
"""Returns the list of file_detail tuples for assets in the apk.
Args:
path_tuples: List of src_path, dest_path tuples to add.
fast_align: Whether to perform alignment
in python zipfile (alternatively
alignment can be done using the zipalign utility out of band).
disable_compression: Whether to disable compression.
allow_reads:
If false, we do
not try to read the files
from disk (to find
their size
for example).
Returns: A list of (src_path, apk_path, compress, alignment) tuple
representing what
and how assets are added.
"""
assets_to_add = []
# Group all uncompressed assets together in the hope that it will increase
# locality of mmap'ed files.
for target_compress
in (
False,
True):
for src_path, dest_path
in path_tuples:
compress =
not disable_compression
and (
os.path.splitext(src_path)[1]
not in _NO_COMPRESS_EXTENSIONS)
if target_compress == compress:
# AddToZipHermetic() uses this logic to avoid growing small files.
# We need it here in order to set alignment correctly.
if allow_reads
and compress
and os.path.getsize(src_path) < 16:
compress =
False
apk_path =
'assets/' + dest_path
alignment = 0
if compress
and not fast_align
else 4
assets_to_add.append((apk_path, src_path, compress, alignment))
return assets_to_add
def _AddFiles(apk, details):
"""Adds files to the apk.
Args:
apk: path to APK to add to.
details: A list of file detail tuples (src_path, apk_path, compress,
alignment) representing what
and how files are added to the APK.
"""
for apk_path, src_path, compress, alignment
in details:
# This check is only relevant for assets, but it should not matter if it is
# checked for the whole list of files.
try:
apk.getinfo(apk_path)
# Should never happen since write_build_config.py handles merging.
raise Exception(
'Multiple targets specified the asset path: %s' % apk_path)
except KeyError:
zipalign.AddToZipHermetic(
apk,
apk_path,
src_path=src_path,
compress=compress,
alignment=alignment)
def _GetNativeLibrariesToAdd(native_libs, android_abi, uncompress, fast_align,
lib_always_compress, lib_renames):
"""Returns the list of file_detail tuples for native libraries in the apk.
Returns: A list of (src_path, apk_path, compress, alignment) tuple
representing what
and how native libraries are added.
"""
libraries_to_add = []
for path
in native_libs:
basename = os.path.basename(path)
compress =
not uncompress
or any(lib_name
in basename
for lib_name
in lib_always_compress)
rename = any(lib_name
in basename
for lib_name
in lib_renames)
if rename:
basename =
'crazy.' + basename
lib_android_abi = android_abi
if path.startswith(
'android_clang_arm64_hwasan/'):
lib_android_abi =
'arm64-v8a-hwasan'
apk_path =
'lib/%s/%s' % (lib_android_abi, basename)
alignment = 0
if compress
and not fast_align
else 0x1000
libraries_to_add.append((apk_path, path, compress, alignment))
return libraries_to_add
def _CreateExpectationsData(native_libs, assets):
"""Creates list of native libraries and assets."""
native_libs = sorted(native_libs)
assets = sorted(assets)
ret = []
for apk_path, _, compress, alignment
in native_libs + assets:
ret.append(
'apk_path=%s, compress=%s, alignment=%s\n' %
(apk_path, compress, alignment))
return ''.join(ret)
def main(args):
build_utils.InitLogging(
'APKBUILDER_DEBUG')
args = build_utils.ExpandFileArgs(args)
options = _ParseArgs(args)
# Until Python 3.7, there's no better way to set compression level.
# The default is 6.
if options.best_compression:
# Compresses about twice as slow as the default.
zlib.Z_DEFAULT_COMPRESSION = 9
else:
# Compresses about twice as fast as the default.
zlib.Z_DEFAULT_COMPRESSION = 1
# Manually align only when alignment is necessary.
# Python's zip implementation duplicates file comments in the central
# directory, whereas zipalign does not, so use zipalign for official builds.
fast_align = options.format ==
'apk' and not options.best_compression
native_libs = sorted(options.native_libs)
# Include native libs in the depfile_deps since GN doesn't know about the
# dependencies when is_component_build=true.
depfile_deps = list(native_libs)
# For targets that depend on static library APKs, dex paths are created by
# the static library's dexsplitter target and GN doesn't know about these
# paths.
if options.dex_file:
depfile_deps.append(options.dex_file)
secondary_native_libs = []
if options.secondary_native_libs:
secondary_native_libs = sorted(options.secondary_native_libs)
depfile_deps += secondary_native_libs
if options.java_resources:
# Included via .build_config.json, so need to write it to depfile.
depfile_deps.extend(options.java_resources)
assets = _ExpandPaths(options.assets)
uncompressed_assets = _ExpandPaths(options.uncompressed_assets)
# Included via .build_config.json, so need to write it to depfile.
depfile_deps.extend(x[0]
for x
in assets)
depfile_deps.extend(x[0]
for x
in uncompressed_assets)
depfile_deps.append(options.resource_apk)
# Bundle modules have a structure similar to APKs, except that resources
# are compiled in protobuf format (instead of binary xml), and that some
# files are located into different top-level directories, e.g.:
# AndroidManifest.xml -> manifest/AndroidManifest.xml
# classes.dex -> dex/classes.dex
# res/ -> res/ (unchanged)
# assets/ -> assets/ (unchanged)
# <other-file> -> root/<other-file>
#
# Hence, the following variables are used to control the location of files in
# the final archive.
if options.format ==
'bundle-module':
apk_manifest_dir =
'manifest/'
apk_root_dir =
'root/'
apk_dex_dir =
'dex/'
else:
apk_manifest_dir =
''
apk_root_dir =
''
apk_dex_dir =
''
def _GetAssetDetails(assets, uncompressed_assets, fast_align, allow_reads):
ret = _GetAssetsToAdd(assets,
fast_align,
disable_compression=
False,
allow_reads=allow_reads)
ret.extend(
_GetAssetsToAdd(uncompressed_assets,
fast_align,
disable_compression=
True,
allow_reads=allow_reads))
return ret
libs_to_add = _GetNativeLibrariesToAdd(
native_libs, options.android_abi, options.uncompress_shared_libraries,
fast_align, options.library_always_compress, options.library_renames)
if options.secondary_android_abi:
libs_to_add.extend(
_GetNativeLibrariesToAdd(
secondary_native_libs, options.secondary_android_abi,
options.uncompress_shared_libraries, fast_align,
options.library_always_compress, options.library_renames))
if options.expected_file:
# We compute expectations without reading the files. This allows us to check
# expectations for different targets by just generating their build_configs
# and not have to first generate all the actual files and all their
# dependencies (for example by just passing --only-verify-expectations).
asset_details = _GetAssetDetails(assets,
uncompressed_assets,
fast_align,
allow_reads=
False)
actual_data = _CreateExpectationsData(libs_to_add, asset_details)
diff_utils.CheckExpectations(actual_data, options)
if options.only_verify_expectations:
if options.depfile:
build_utils.WriteDepfile(options.depfile,
options.actual_file,
inputs=depfile_deps)
return
# If we are past this point, we are going to actually create the final apk so
# we should recompute asset details again but maybe perform some optimizations
# based on the size of the files on disk.
assets_to_add = _GetAssetDetails(
assets, uncompressed_assets, fast_align, allow_reads=
True)
# Targets generally do not depend on apks, so no need for only_if_changed.
with build_utils.AtomicOutput(options.output_apk, only_if_changed=
False)
as f:
with zipfile.ZipFile(options.resource_apk)
as resource_apk, \
zipfile.ZipFile(f,
'w')
as out_apk:
def add_to_zip(zip_path, data, compress=
True, alignment=4):
zipalign.AddToZipHermetic(
out_apk,
zip_path,
data=data,
compress=compress,
alignment=0
if compress
and not fast_align
else alignment)
def copy_resource(zipinfo, out_dir=
''):
add_to_zip(
out_dir + zipinfo.filename,
resource_apk.read(zipinfo.filename),
compress=zipinfo.compress_type != zipfile.ZIP_STORED)
# Make assets come before resources in order to maintain the same file
# ordering as GYP / aapt. http://crbug.com/561862
resource_infos = resource_apk.infolist()
# 1. AndroidManifest.xml
logging.debug(
'Adding AndroidManifest.xml')
copy_resource(
resource_apk.getinfo(
'AndroidManifest.xml'), out_dir=apk_manifest_dir)
# 2. Assets
logging.debug(
'Adding assets/')
_AddFiles(out_apk, assets_to_add)
# 3. Dex files
logging.debug(
'Adding classes.dex')
if options.dex_file:
with open(options.dex_file,
'rb')
as dex_file_obj:
if options.dex_file.endswith(
'.dex'):
max_dex_number = 1
# This is the case for incremental_install=true.
add_to_zip(
apk_dex_dir +
'classes.dex',
dex_file_obj.read(),
compress=
not options.uncompress_dex)
else:
max_dex_number = 0
with zipfile.ZipFile(dex_file_obj)
as dex_zip:
for dex
in (d
for d
in dex_zip.namelist()
if d.endswith(
'.dex')):
max_dex_number += 1
add_to_zip(
apk_dex_dir + dex,
dex_zip.read(dex),
compress=
not options.uncompress_dex)
if options.jdk_libs_dex_file:
with open(options.jdk_libs_dex_file,
'rb')
as dex_file_obj:
add_to_zip(
apk_dex_dir +
'classes{}.dex'.format(max_dex_number + 1),
dex_file_obj.read(),
compress=
not options.uncompress_dex)
# 4. Native libraries.
logging.debug(
'Adding lib/')
_AddFiles(out_apk, libs_to_add)
# Add a placeholder lib if the APK should be multi ABI but is missing libs
# for one of the ABIs.
native_lib_placeholders = options.native_lib_placeholders
secondary_native_lib_placeholders = (
options.secondary_native_lib_placeholders)
if options.is_multi_abi:
if ((secondary_native_libs
or secondary_native_lib_placeholders)
and not native_libs
and not native_lib_placeholders):
native_lib_placeholders += [
'libplaceholder.so']
if ((native_libs
or native_lib_placeholders)
and not secondary_native_libs
and not secondary_native_lib_placeholders):
secondary_native_lib_placeholders += [
'libplaceholder.so']
# Add placeholder libs.
for name
in sorted(native_lib_placeholders):
# Note: Empty libs files are ignored by md5check (can cause issues
# with stale builds when the only change is adding/removing
# placeholders).
apk_path =
'lib/%s/%s' % (options.android_abi, name)
add_to_zip(apk_path,
'', alignment=0x1000)
for name
in sorted(secondary_native_lib_placeholders):
# Note: Empty libs files are ignored by md5check (can cause issues
# with stale builds when the only change is adding/removing
# placeholders).
apk_path =
'lib/%s/%s' % (options.secondary_android_abi, name)
add_to_zip(apk_path,
'', alignment=0x1000)
# 5. Resources
logging.debug(
'Adding res/')
for info
in sorted(resource_infos, key=
lambda i: i.filename):
if info.filename !=
'AndroidManifest.xml':
copy_resource(info)
# 6. Java resources that should be accessible via
# Class.getResourceAsStream(), in particular parts of Emma jar.
# Prebuilt jars may contain class files which we shouldn't include.
logging.debug(
'Adding Java resources')
for java_resource
in options.java_resources:
with zipfile.ZipFile(java_resource,
'r')
as java_resource_jar:
for apk_path
in sorted(java_resource_jar.namelist()):
apk_path_lower = apk_path.lower()
if apk_path_lower.startswith(
'meta-inf/'):
continue
if apk_path_lower.endswith(
'/'):
continue
if apk_path_lower.endswith(
'.class'):
continue
add_to_zip(apk_root_dir + apk_path,
java_resource_jar.read(apk_path))
if options.format ==
'apk':
zipalign_path =
None if fast_align
else options.zipalign_path
finalize_apk.FinalizeApk(options.apksigner_jar,
zipalign_path,
f.name,
f.name,
options.key_path,
options.key_passwd,
options.key_name,
int(options.min_sdk_version),
warnings_as_errors=options.warnings_as_errors)
logging.debug(
'Moving file into place')
if options.depfile:
build_utils.WriteDepfile(options.depfile,
options.output_apk,
inputs=depfile_deps)
if __name__ ==
'__main__':
main(sys.argv[1:])