blob: 6c429118a018554ff5ce0122191ec9f060079da6 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 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.
"""Install *_incremental.apk targets as well as their dependent files."""
import argparse
import glob
import logging
import os
import posixpath
import shutil
import sys
import zipfile
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
import devil_chromium
from devil.android import apk_helper
from devil.android import device_utils
from devil.android import device_errors
from devil.android.sdk import version_codes
from devil.utils import reraiser_thread
from pylib import constants
from pylib.utils import run_tests_helper
from pylib.utils import time_profile
prev_sys_path = list(sys.path)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
from util import build_utils
sys.path = prev_sys_path
def _DeviceCachePath(device):
file_name = 'device_cache_%s.json' % device.adb.GetDeviceSerial()
return os.path.join(constants.GetOutDirectory(), file_name)
def _TransformDexPaths(paths):
"""Given paths like ["/a/b/c", "/a/c/d"], returns ["b.c", "c.d"]."""
if len(paths) == 1:
return [os.path.basename(paths[0])]
prefix_len = len(os.path.commonprefix(paths))
return [p[prefix_len:].replace(os.sep, '.') for p in paths]
def _Execute(concurrently, *funcs):
"""Calls all functions in |funcs| concurrently or in sequence."""
timer = time_profile.TimeProfile()
if concurrently:
reraiser_thread.RunAsync(funcs)
else:
for f in funcs:
f()
timer.Stop(log=False)
return timer
def _GetDeviceIncrementalDir(package):
"""Returns the device path to put incremental files for the given package."""
return '/data/local/tmp/incremental-app-%s' % package
def _HasClasses(jar_path):
"""Returns whether the given jar contains classes.dex."""
with zipfile.ZipFile(jar_path) as jar:
return 'classes.dex' in jar.namelist()
def Uninstall(device, package, enable_device_cache=False):
"""Uninstalls and removes all incremental files for the given package."""
main_timer = time_profile.TimeProfile()
device.Uninstall(package)
if enable_device_cache:
# Uninstall is rare, so just wipe the cache in this case.
cache_path = _DeviceCachePath(device)
if os.path.exists(cache_path):
os.unlink(cache_path)
device.RunShellCommand(['rm', '-rf', _GetDeviceIncrementalDir(package)],
check_return=True)
logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
def Install(device, apk, split_globs=None, native_libs=None, dex_files=None,
enable_device_cache=False, use_concurrency=True,
show_proguard_warning=False, permissions=(),
allow_downgrade=True):
"""Installs the given incremental apk and all required supporting files.
Args:
device: A DeviceUtils instance.
apk: The path to the apk, or an ApkHelper instance.
split_globs: Glob patterns for any required apk splits (optional).
native_libs: List of app's native libraries (optional).
dex_files: List of .dex.jar files that comprise the app's Dalvik code.
enable_device_cache: Whether to enable on-device caching of checksums.
use_concurrency: Whether to speed things up using multiple threads.
show_proguard_warning: Whether to print a warning about Proguard not being
enabled after installing.
permissions: A list of the permissions to grant, or None to grant all
non-blacklisted permissions in the manifest.
"""
main_timer = time_profile.TimeProfile()
install_timer = time_profile.TimeProfile()
push_native_timer = time_profile.TimeProfile()
push_dex_timer = time_profile.TimeProfile()
apk = apk_helper.ToHelper(apk)
apk_package = apk.GetPackageName()
device_incremental_dir = _GetDeviceIncrementalDir(apk_package)
# Install .apk(s) if any of them have changed.
def do_install():
install_timer.Start()
if split_globs:
splits = []
for split_glob in split_globs:
splits.extend((f for f in glob.glob(split_glob)))
device.InstallSplitApk(apk, splits, reinstall=True,
allow_cached_props=True, permissions=permissions,
allow_downgrade=allow_downgrade)
else:
device.Install(apk, reinstall=True, permissions=permissions,
allow_downgrade=allow_downgrade)
install_timer.Stop(log=False)
# Push .so and .dex files to the device (if they have changed).
def do_push_files():
if native_libs:
push_native_timer.Start()
with build_utils.TempDir() as temp_dir:
device_lib_dir = posixpath.join(device_incremental_dir, 'lib')
for path in native_libs:
# Note: Can't use symlinks as they don't work when
# "adb push parent_dir" is used (like we do here).
shutil.copy(path, os.path.join(temp_dir, os.path.basename(path)))
device.PushChangedFiles([(temp_dir, device_lib_dir)],
delete_device_stale=True)
push_native_timer.Stop(log=False)
if dex_files:
push_dex_timer.Start()
# Put all .dex files to be pushed into a temporary directory so that we
# can use delete_device_stale=True.
with build_utils.TempDir() as temp_dir:
device_dex_dir = posixpath.join(device_incremental_dir, 'dex')
# Ensure no two files have the same name.
transformed_names = _TransformDexPaths(dex_files)
for src_path, dest_name in zip(dex_files, transformed_names):
# Binary targets with no extra classes create .dex.jar without a
# classes.dex (which Android chokes on).
if _HasClasses(src_path):
shutil.copy(src_path, os.path.join(temp_dir, dest_name))
device.PushChangedFiles([(temp_dir, device_dex_dir)],
delete_device_stale=True)
push_dex_timer.Stop(log=False)
def check_selinux():
# Marshmallow has no filesystem access whatsoever. It might be possible to
# get things working on Lollipop, but attempts so far have failed.
# http://crbug.com/558818
has_selinux = device.build_version_sdk >= version_codes.LOLLIPOP
if has_selinux and apk.HasIsolatedProcesses():
raise Exception('Cannot use incremental installs on Android L+ without '
'first disabling isoloated processes.\n'
'To do so, use GN arg:\n'
' disable_incremental_isolated_processes=true')
cache_path = _DeviceCachePath(device)
def restore_cache():
if not enable_device_cache:
logging.info('Ignoring device cache')
return
if os.path.exists(cache_path):
logging.info('Using device cache: %s', cache_path)
with open(cache_path) as f:
device.LoadCacheData(f.read())
# Delete the cached file so that any exceptions cause it to be cleared.
os.unlink(cache_path)
else:
logging.info('No device cache present: %s', cache_path)
def save_cache():
with open(cache_path, 'w') as f:
f.write(device.DumpCacheData())
logging.info('Wrote device cache: %s', cache_path)
# Create 2 lock files:
# * install.lock tells the app to pause on start-up (until we release it).
# * firstrun.lock is used by the app to pause all secondary processes until
# the primary process finishes loading the .dex / .so files.
def create_lock_files():
# Creates or zeros out lock files.
cmd = ('D="%s";'
'mkdir -p $D &&'
'echo -n >$D/install.lock 2>$D/firstrun.lock')
device.RunShellCommand(cmd % device_incremental_dir, check_return=True)
# The firstrun.lock is released by the app itself.
def release_installer_lock():
device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir,
check_return=True)
# Concurrency here speeds things up quite a bit, but DeviceUtils hasn't
# been designed for multi-threading. Enabling only because this is a
# developer-only tool.
setup_timer = _Execute(
use_concurrency, create_lock_files, restore_cache, check_selinux)
_Execute(use_concurrency, do_install, do_push_files)
finalize_timer = _Execute(use_concurrency, release_installer_lock, save_cache)
logging.info(
'Took %s seconds (setup=%s, install=%s, libs=%s, dex=%s, finalize=%s)',
main_timer.GetDelta(), setup_timer.GetDelta(), install_timer.GetDelta(),
push_native_timer.GetDelta(), push_dex_timer.GetDelta(),
finalize_timer.GetDelta())
if show_proguard_warning:
logging.warning('Target had proguard enabled, but incremental install uses '
'non-proguarded .dex files. Performance characteristics '
'may differ.')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('apk_path',
help='The path to the APK to install.')
parser.add_argument('--split',
action='append',
dest='splits',
help='A glob matching the apk splits. '
'Can be specified multiple times.')
parser.add_argument('--native_lib',
dest='native_libs',
help='Path to native library (repeatable)',
action='append',
default=[])
parser.add_argument('--dex-file',
dest='dex_files',
help='Path to dex files (repeatable)',
action='append',
default=[])
parser.add_argument('-d', '--device', dest='device',
help='Target device for apk to install on.')
parser.add_argument('--uninstall',
action='store_true',
default=False,
help='Remove the app and all side-loaded files.')
parser.add_argument('--output-directory',
help='Path to the root build directory.')
parser.add_argument('--no-threading',
action='store_false',
default=True,
dest='threading',
help='Do not install and push concurrently')
parser.add_argument('--no-cache',
action='store_false',
default=True,
dest='cache',
help='Do not use cached information about what files are '
'currently on the target device.')
parser.add_argument('--show-proguard-warning',
action='store_true',
default=False,
help='Print a warning about proguard being disabled')
parser.add_argument('--dont-even-try',
help='Prints this message and exits.')
parser.add_argument('-v',
'--verbose',
dest='verbose_count',
default=0,
action='count',
help='Verbose level (multiple times for more)')
parser.add_argument('--disable-downgrade',
action='store_false',
default=True,
dest='allow_downgrade',
help='Disable install of apk with lower version number'
'than the version already on the device.')
args = parser.parse_args()
run_tests_helper.SetLogLevel(args.verbose_count)
constants.SetBuildType('Debug')
if args.output_directory:
constants.SetOutputDirectory(args.output_directory)
devil_chromium.Initialize(output_directory=constants.GetOutDirectory())
if args.dont_even_try:
logging.fatal(args.dont_even_try)
return 1
# Retries are annoying when commands fail for legitimate reasons. Might want
# to enable them if this is ever used on bots though.
device = device_utils.DeviceUtils.HealthyDevices(
device_arg=args.device,
default_retries=0,
enable_device_files_cache=True)[0]
apk = apk_helper.ToHelper(args.apk_path)
if args.uninstall:
Uninstall(device, apk.GetPackageName(), enable_device_cache=args.cache)
else:
Install(device, apk, split_globs=args.splits, native_libs=args.native_libs,
dex_files=args.dex_files, enable_device_cache=args.cache,
use_concurrency=args.threading,
show_proguard_warning=args.show_proguard_warning,
allow_downgrade=args.allow_downgrade)
if __name__ == '__main__':
sys.exit(main())