Home | History | Annotate | Download | only in incremental_install
      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 
      9 import argparse
     10 import glob
     11 import logging
     12 import os
     13 import posixpath
     14 import shutil
     15 import sys
     16 import zipfile
     17 
     18 sys.path.append(
     19     os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
     20 import devil_chromium
     21 from devil.android import apk_helper
     22 from devil.android import device_utils
     23 from devil.android import device_errors
     24 from devil.android.sdk import version_codes
     25 from devil.utils import reraiser_thread
     26 from pylib import constants
     27 from pylib.utils import run_tests_helper
     28 from pylib.utils import time_profile
     29 
     30 prev_sys_path = list(sys.path)
     31 sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
     32 from util import build_utils
     33 sys.path = prev_sys_path
     34 
     35 
     36 def _DeviceCachePath(device):
     37   file_name = 'device_cache_%s.json' % device.adb.GetDeviceSerial()
     38   return os.path.join(constants.GetOutDirectory(), file_name)
     39 
     40 
     41 def _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 
     50 def _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 
     62 def _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 
     67 def _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 
     73 def 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 
     87 def 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 
    228 def 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 
    316 if __name__ == '__main__':
    317   sys.exit(main())
    318