1#!/usr/bin/env python 2# 3# Copyright 2015 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Install *_incremental.apk targets as well as their dependent files.""" 8 9import argparse 10import glob 11import logging 12import os 13import posixpath 14import shutil 15import sys 16import zipfile 17 18sys.path.append( 19 os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) 20import devil_chromium 21from devil.android import apk_helper 22from devil.android import device_utils 23from devil.android import device_errors 24from devil.android.sdk import version_codes 25from devil.utils import reraiser_thread 26from pylib import constants 27from pylib.utils import run_tests_helper 28from pylib.utils import time_profile 29 30prev_sys_path = list(sys.path) 31sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'gyp')) 32from util import build_utils 33sys.path = prev_sys_path 34 35 36def _DeviceCachePath(device): 37 file_name = 'device_cache_%s.json' % device.adb.GetDeviceSerial() 38 return os.path.join(constants.GetOutDirectory(), file_name) 39 40 41def _TransformDexPaths(paths): 42 """Given paths like ["/a/b/c", "/a/c/d"], returns ["b.c", "c.d"].""" 43 if len(paths) == 1: 44 return [os.path.basename(paths[0])] 45 46 prefix_len = len(os.path.commonprefix(paths)) 47 return [p[prefix_len:].replace(os.sep, '.') for p in paths] 48 49 50def _Execute(concurrently, *funcs): 51 """Calls all functions in |funcs| concurrently or in sequence.""" 52 timer = time_profile.TimeProfile() 53 if concurrently: 54 reraiser_thread.RunAsync(funcs) 55 else: 56 for f in funcs: 57 f() 58 timer.Stop(log=False) 59 return timer 60 61 62def _GetDeviceIncrementalDir(package): 63 """Returns the device path to put incremental files for the given package.""" 64 return '/data/local/tmp/incremental-app-%s' % package 65 66 67def _HasClasses(jar_path): 68 """Returns whether the given jar contains classes.dex.""" 69 with zipfile.ZipFile(jar_path) as jar: 70 return 'classes.dex' in jar.namelist() 71 72 73def Uninstall(device, package, enable_device_cache=False): 74 """Uninstalls and removes all incremental files for the given package.""" 75 main_timer = time_profile.TimeProfile() 76 device.Uninstall(package) 77 if enable_device_cache: 78 # Uninstall is rare, so just wipe the cache in this case. 79 cache_path = _DeviceCachePath(device) 80 if os.path.exists(cache_path): 81 os.unlink(cache_path) 82 device.RunShellCommand(['rm', '-rf', _GetDeviceIncrementalDir(package)], 83 check_return=True) 84 logging.info('Uninstall took %s seconds.', main_timer.GetDelta()) 85 86 87def Install(device, apk, split_globs=None, native_libs=None, dex_files=None, 88 enable_device_cache=False, use_concurrency=True, 89 show_proguard_warning=False, permissions=(), 90 allow_downgrade=True): 91 """Installs the given incremental apk and all required supporting files. 92 93 Args: 94 device: A DeviceUtils instance. 95 apk: The path to the apk, or an ApkHelper instance. 96 split_globs: Glob patterns for any required apk splits (optional). 97 native_libs: List of app's native libraries (optional). 98 dex_files: List of .dex.jar files that comprise the app's Dalvik code. 99 enable_device_cache: Whether to enable on-device caching of checksums. 100 use_concurrency: Whether to speed things up using multiple threads. 101 show_proguard_warning: Whether to print a warning about Proguard not being 102 enabled after installing. 103 permissions: A list of the permissions to grant, or None to grant all 104 non-blacklisted permissions in the manifest. 105 """ 106 main_timer = time_profile.TimeProfile() 107 install_timer = time_profile.TimeProfile() 108 push_native_timer = time_profile.TimeProfile() 109 push_dex_timer = time_profile.TimeProfile() 110 111 apk = apk_helper.ToHelper(apk) 112 apk_package = apk.GetPackageName() 113 device_incremental_dir = _GetDeviceIncrementalDir(apk_package) 114 115 # Install .apk(s) if any of them have changed. 116 def do_install(): 117 install_timer.Start() 118 if split_globs: 119 splits = [] 120 for split_glob in split_globs: 121 splits.extend((f for f in glob.glob(split_glob))) 122 device.InstallSplitApk(apk, splits, reinstall=True, 123 allow_cached_props=True, permissions=permissions, 124 allow_downgrade=allow_downgrade) 125 else: 126 device.Install(apk, reinstall=True, permissions=permissions, 127 allow_downgrade=allow_downgrade) 128 install_timer.Stop(log=False) 129 130 # Push .so and .dex files to the device (if they have changed). 131 def do_push_files(): 132 if native_libs: 133 push_native_timer.Start() 134 with build_utils.TempDir() as temp_dir: 135 device_lib_dir = posixpath.join(device_incremental_dir, 'lib') 136 for path in native_libs: 137 # Note: Can't use symlinks as they don't work when 138 # "adb push parent_dir" is used (like we do here). 139 shutil.copy(path, os.path.join(temp_dir, os.path.basename(path))) 140 device.PushChangedFiles([(temp_dir, device_lib_dir)], 141 delete_device_stale=True) 142 push_native_timer.Stop(log=False) 143 144 if dex_files: 145 push_dex_timer.Start() 146 # Put all .dex files to be pushed into a temporary directory so that we 147 # can use delete_device_stale=True. 148 with build_utils.TempDir() as temp_dir: 149 device_dex_dir = posixpath.join(device_incremental_dir, 'dex') 150 # Ensure no two files have the same name. 151 transformed_names = _TransformDexPaths(dex_files) 152 for src_path, dest_name in zip(dex_files, transformed_names): 153 # Binary targets with no extra classes create .dex.jar without a 154 # classes.dex (which Android chokes on). 155 if _HasClasses(src_path): 156 shutil.copy(src_path, os.path.join(temp_dir, dest_name)) 157 device.PushChangedFiles([(temp_dir, device_dex_dir)], 158 delete_device_stale=True) 159 push_dex_timer.Stop(log=False) 160 161 def check_selinux(): 162 # Marshmallow has no filesystem access whatsoever. It might be possible to 163 # get things working on Lollipop, but attempts so far have failed. 164 # http://crbug.com/558818 165 has_selinux = device.build_version_sdk >= version_codes.LOLLIPOP 166 if has_selinux and apk.HasIsolatedProcesses(): 167 raise Exception('Cannot use incremental installs on Android L+ without ' 168 'first disabling isoloated processes.\n' 169 'To do so, use GN arg:\n' 170 ' disable_incremental_isolated_processes=true') 171 172 cache_path = _DeviceCachePath(device) 173 def restore_cache(): 174 if not enable_device_cache: 175 logging.info('Ignoring device cache') 176 return 177 if os.path.exists(cache_path): 178 logging.info('Using device cache: %s', cache_path) 179 with open(cache_path) as f: 180 device.LoadCacheData(f.read()) 181 # Delete the cached file so that any exceptions cause it to be cleared. 182 os.unlink(cache_path) 183 else: 184 logging.info('No device cache present: %s', cache_path) 185 186 def save_cache(): 187 with open(cache_path, 'w') as f: 188 f.write(device.DumpCacheData()) 189 logging.info('Wrote device cache: %s', cache_path) 190 191 # Create 2 lock files: 192 # * install.lock tells the app to pause on start-up (until we release it). 193 # * firstrun.lock is used by the app to pause all secondary processes until 194 # the primary process finishes loading the .dex / .so files. 195 def create_lock_files(): 196 # Creates or zeros out lock files. 197 cmd = ('D="%s";' 198 'mkdir -p $D &&' 199 'echo -n >$D/install.lock 2>$D/firstrun.lock') 200 device.RunShellCommand(cmd % device_incremental_dir, check_return=True) 201 202 # The firstrun.lock is released by the app itself. 203 def release_installer_lock(): 204 device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir, 205 check_return=True) 206 207 # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't 208 # been designed for multi-threading. Enabling only because this is a 209 # developer-only tool. 210 setup_timer = _Execute( 211 use_concurrency, create_lock_files, restore_cache, check_selinux) 212 213 _Execute(use_concurrency, do_install, do_push_files) 214 215 finalize_timer = _Execute(use_concurrency, release_installer_lock, save_cache) 216 217 logging.info( 218 'Took %s seconds (setup=%s, install=%s, libs=%s, dex=%s, finalize=%s)', 219 main_timer.GetDelta(), setup_timer.GetDelta(), install_timer.GetDelta(), 220 push_native_timer.GetDelta(), push_dex_timer.GetDelta(), 221 finalize_timer.GetDelta()) 222 if show_proguard_warning: 223 logging.warning('Target had proguard enabled, but incremental install uses ' 224 'non-proguarded .dex files. Performance characteristics ' 225 'may differ.') 226 227 228def main(): 229 parser = argparse.ArgumentParser() 230 parser.add_argument('apk_path', 231 help='The path to the APK to install.') 232 parser.add_argument('--split', 233 action='append', 234 dest='splits', 235 help='A glob matching the apk splits. ' 236 'Can be specified multiple times.') 237 parser.add_argument('--native_lib', 238 dest='native_libs', 239 help='Path to native library (repeatable)', 240 action='append', 241 default=[]) 242 parser.add_argument('--dex-file', 243 dest='dex_files', 244 help='Path to dex files (repeatable)', 245 action='append', 246 default=[]) 247 parser.add_argument('-d', '--device', dest='device', 248 help='Target device for apk to install on.') 249 parser.add_argument('--uninstall', 250 action='store_true', 251 default=False, 252 help='Remove the app and all side-loaded files.') 253 parser.add_argument('--output-directory', 254 help='Path to the root build directory.') 255 parser.add_argument('--no-threading', 256 action='store_false', 257 default=True, 258 dest='threading', 259 help='Do not install and push concurrently') 260 parser.add_argument('--no-cache', 261 action='store_false', 262 default=True, 263 dest='cache', 264 help='Do not use cached information about what files are ' 265 'currently on the target device.') 266 parser.add_argument('--show-proguard-warning', 267 action='store_true', 268 default=False, 269 help='Print a warning about proguard being disabled') 270 parser.add_argument('--dont-even-try', 271 help='Prints this message and exits.') 272 parser.add_argument('-v', 273 '--verbose', 274 dest='verbose_count', 275 default=0, 276 action='count', 277 help='Verbose level (multiple times for more)') 278 parser.add_argument('--disable-downgrade', 279 action='store_false', 280 default=True, 281 dest='allow_downgrade', 282 help='Disable install of apk with lower version number' 283 'than the version already on the device.') 284 285 args = parser.parse_args() 286 287 run_tests_helper.SetLogLevel(args.verbose_count) 288 constants.SetBuildType('Debug') 289 if args.output_directory: 290 constants.SetOutputDirectory(args.output_directory) 291 292 devil_chromium.Initialize(output_directory=constants.GetOutDirectory()) 293 294 if args.dont_even_try: 295 logging.fatal(args.dont_even_try) 296 return 1 297 298 # Retries are annoying when commands fail for legitimate reasons. Might want 299 # to enable them if this is ever used on bots though. 300 device = device_utils.DeviceUtils.HealthyDevices( 301 device_arg=args.device, 302 default_retries=0, 303 enable_device_files_cache=True)[0] 304 305 apk = apk_helper.ToHelper(args.apk_path) 306 if args.uninstall: 307 Uninstall(device, apk.GetPackageName(), enable_device_cache=args.cache) 308 else: 309 Install(device, apk, split_globs=args.splits, native_libs=args.native_libs, 310 dex_files=args.dex_files, enable_device_cache=args.cache, 311 use_concurrency=args.threading, 312 show_proguard_warning=args.show_proguard_warning, 313 allow_downgrade=args.allow_downgrade) 314 315 316if __name__ == '__main__': 317 sys.exit(main()) 318