Roll src/third_party/WebKit a3b4a2e:7441784 (svn 202551:202552)
[chromium-blink-merge.git] / build / android / incremental_install / installer.py
blob1d70335491e3016a155c6763e8ff25bd9817df31
1 #!/usr/bin/env python
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.
7 """Install *_incremental.apk targets as well as their dependent files."""
9 import argparse
10 import glob
11 import logging
12 import os
13 import posixpath
14 import shutil
15 import sys
17 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
18 from devil.android import apk_helper
19 from devil.android import device_utils
20 from devil.android import device_errors
21 from devil.android.sdk import version_codes
22 from devil.utils import reraiser_thread
23 from pylib import constants
24 from pylib.utils import run_tests_helper
25 from pylib.utils import time_profile
27 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
28 from util import build_utils
31 def _TransformDexPaths(paths):
32 """Given paths like ["/a/b/c", "/a/c/d"], returns ["b.c", "c.d"]."""
33 prefix_len = len(os.path.commonprefix(paths))
34 return [p[prefix_len:].replace(os.sep, '.') for p in paths]
37 def main():
38 parser = argparse.ArgumentParser()
39 parser.add_argument('apk_path',
40 help='The path to the APK to install.')
41 parser.add_argument('--split',
42 action='append',
43 dest='splits',
44 help='A glob matching the apk splits. '
45 'Can be specified multiple times.')
46 parser.add_argument('--lib-dir',
47 help='Path to native libraries directory.')
48 parser.add_argument('--dex-files',
49 help='List of dex files to push.',
50 action='append',
51 default=[])
52 parser.add_argument('-d', '--device', dest='device',
53 help='Target device for apk to install on.')
54 parser.add_argument('--uninstall',
55 action='store_true',
56 default=False,
57 help='Remove the app and all side-loaded files.')
58 parser.add_argument('--output-directory',
59 help='Path to the root build directory.')
60 parser.add_argument('--no-threading',
61 action='store_true',
62 default=False,
63 help='Do not install and push concurrently')
64 parser.add_argument('-v',
65 '--verbose',
66 dest='verbose_count',
67 default=0,
68 action='count',
69 help='Verbose level (multiple times for more)')
71 args = parser.parse_args()
73 run_tests_helper.SetLogLevel(args.verbose_count)
74 constants.SetBuildType('Debug')
75 if args.output_directory:
76 constants.SetOutputDirectory(args.output_directory)
78 main_timer = time_profile.TimeProfile()
79 install_timer = time_profile.TimeProfile()
80 push_native_timer = time_profile.TimeProfile()
81 push_dex_timer = time_profile.TimeProfile()
83 if args.device:
84 # Retries are annoying when commands fail for legitimate reasons. Might want
85 # to enable them if this is ever used on bots though.
86 device = device_utils.DeviceUtils(args.device, default_retries=0)
87 else:
88 devices = device_utils.DeviceUtils.HealthyDevices(default_retries=0)
89 if not devices:
90 raise device_errors.NoDevicesError()
91 elif len(devices) == 1:
92 device = devices[0]
93 else:
94 all_devices = device_utils.DeviceUtils.parallel(devices)
95 msg = ('More than one device available.\n'
96 'Use --device=SERIAL to select a device.\n'
97 'Available devices:\n')
98 descriptions = all_devices.pMap(lambda d: d.build_description).pGet(None)
99 for d, desc in zip(devices, descriptions):
100 msg += ' %s (%s)\n' % (d, desc)
101 raise Exception(msg)
103 apk_help = apk_helper.ApkHelper(args.apk_path)
104 apk_package = apk_help.GetPackageName()
105 device_incremental_dir = '/data/local/tmp/incremental-app-%s' % apk_package
107 if args.uninstall:
108 device.Uninstall(apk_package)
109 device.RunShellCommand(['rm', '-rf', device_incremental_dir],
110 check_return=True)
111 logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
112 return
114 if device.build_version_sdk >= version_codes.MARSHMALLOW:
115 if apk_help.HasIsolatedProcesses():
116 raise Exception('Cannot use perform incremental installs on Android M+ '
117 'without first disabling isolated processes. Use GN arg: '
118 'disable_incremental_isolated_processes=true to do so.')
120 # Install .apk(s) if any of them have changed.
121 def do_install():
122 install_timer.Start()
123 if args.splits:
124 splits = []
125 for split_glob in args.splits:
126 splits.extend((f for f in glob.glob(split_glob)))
127 device.InstallSplitApk(args.apk_path, splits, reinstall=True,
128 allow_cached_props=True)
129 else:
130 device.Install(args.apk_path, reinstall=True)
131 install_timer.Stop(log=False)
133 # Push .so and .dex files to the device (if they have changed).
134 def do_push_files():
135 if args.lib_dir:
136 push_native_timer.Start()
137 device_lib_dir = posixpath.join(device_incremental_dir, 'lib')
138 device.PushChangedFiles([(args.lib_dir, device_lib_dir)],
139 delete_device_stale=True)
140 push_native_timer.Stop(log=False)
142 if args.dex_files:
143 push_dex_timer.Start()
144 # Put all .dex files to be pushed into a temporary directory so that we
145 # can use delete_device_stale=True.
146 with build_utils.TempDir() as temp_dir:
147 device_dex_dir = posixpath.join(device_incremental_dir, 'dex')
148 # Ensure no two files have the same name.
149 transformed_names = _TransformDexPaths(args.dex_files)
150 for src_path, dest_name in zip(args.dex_files, transformed_names):
151 shutil.copyfile(src_path, os.path.join(temp_dir, dest_name))
152 device.PushChangedFiles([(temp_dir, device_dex_dir)],
153 delete_device_stale=True)
154 push_dex_timer.Stop(log=False)
156 # Create 2 lock files:
157 # * install.lock tells the app to pause on start-up (until we release it).
158 # * firstrun.lock is used by the app to pause all secondary processes until
159 # the primary process finishes loading the .dex / .so files.
160 def create_lock_files():
161 # Creates or zeros out lock files.
162 cmd = ('D="%s";'
163 'mkdir -p $D &&'
164 'echo -n >$D/install.lock 2>$D/firstrun.lock')
165 device.RunShellCommand(cmd % device_incremental_dir, check_return=True)
167 # The firstrun.lock is released by the app itself.
168 def release_installer_lock():
169 device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir,
170 check_return=True)
172 create_lock_files()
173 # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't
174 # been designed for multi-threading. Enabling only because this is a
175 # developer-only tool.
176 if args.no_threading:
177 do_install()
178 do_push_files()
179 else:
180 reraiser_thread.RunAsync((do_install, do_push_files))
181 release_installer_lock()
182 logging.info('Took %s seconds (install=%s, libs=%s, dex=%s)',
183 main_timer.GetDelta(), install_timer.GetDelta(),
184 push_native_timer.GetDelta(), push_dex_timer.GetDelta())
187 if __name__ == '__main__':
188 sys.exit(main())