• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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