561 lines
22 KiB
Python
Executable file
561 lines
22 KiB
Python
Executable file
#!/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:])
|