OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 """Install *_incremental.apk targets as well as their dependent files.""" | 7 """Install *_incremental.apk targets as well as their dependent files.""" |
8 | 8 |
9 import argparse | 9 import argparse |
10 import glob | 10 import glob |
11 import logging | 11 import logging |
12 import os | |
12 import posixpath | 13 import posixpath |
14 import shutil | |
13 import sys | 15 import sys |
14 import time | 16 |
17 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) | |
18 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp')) | |
jbudorick
2015/09/15 18:36:41
nit: I'd prefer this one down above the build_util
agrieve
2015/09/15 19:30:05
Done.
| |
15 | 19 |
16 from devil.android import apk_helper | 20 from devil.android import apk_helper |
17 from devil.android import device_utils | 21 from devil.android import device_utils |
18 from devil.android import device_errors | 22 from devil.android import device_errors |
23 from devil.android.sdk import version_codes | |
19 from devil.utils import reraiser_thread | 24 from devil.utils import reraiser_thread |
20 from pylib import constants | 25 from pylib import constants |
21 from pylib.utils import run_tests_helper | 26 from pylib.utils import run_tests_helper |
27 from pylib.utils import time_profile | |
28 from util import build_utils | |
29 | |
30 | |
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] | |
22 | 35 |
23 | 36 |
24 def main(): | 37 def main(): |
25 start_time = time.time() | |
26 parser = argparse.ArgumentParser() | 38 parser = argparse.ArgumentParser() |
27 parser.add_argument('apk_path', | 39 parser.add_argument('apk_path', |
28 help='The path to the APK to install.') | 40 help='The path to the APK to install.') |
29 parser.add_argument('--split', | 41 parser.add_argument('--split', |
30 action='append', | 42 action='append', |
31 dest='splits', | 43 dest='splits', |
32 help='A glob matching the apk splits. ' | 44 help='A glob matching the apk splits. ' |
33 'Can be specified multiple times.') | 45 'Can be specified multiple times.') |
34 parser.add_argument('--lib-dir', | 46 parser.add_argument('--lib-dir', |
35 help='Path to native libraries directory.') | 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=[]) | |
36 parser.add_argument('-d', '--device', dest='device', | 52 parser.add_argument('-d', '--device', dest='device', |
37 help='Target device for apk to install on.') | 53 help='Target device for apk to install on.') |
38 parser.add_argument('--uninstall', | 54 parser.add_argument('--uninstall', |
39 action='store_true', | 55 action='store_true', |
40 default=False, | 56 default=False, |
41 help='Remove the app and all side-loaded files.') | 57 help='Remove the app and all side-loaded files.') |
42 parser.add_argument('--no-threading', | 58 parser.add_argument('--no-threading', |
43 action='store_true', | 59 action='store_true', |
44 default=False, | 60 default=False, |
45 help='Do not install and push concurrently') | 61 help='Do not install and push concurrently') |
46 parser.add_argument('-v', | 62 parser.add_argument('-v', |
47 '--verbose', | 63 '--verbose', |
48 dest='verbose_count', | 64 dest='verbose_count', |
49 default=0, | 65 default=0, |
50 action='count', | 66 action='count', |
51 help='Verbose level (multiple times for more)') | 67 help='Verbose level (multiple times for more)') |
52 | 68 |
53 args = parser.parse_args() | 69 args = parser.parse_args() |
54 | 70 |
55 logging.basicConfig(format='%(asctime)s (%(thread)d) %(message)s') | |
56 run_tests_helper.SetLogLevel(args.verbose_count) | 71 run_tests_helper.SetLogLevel(args.verbose_count) |
57 constants.SetBuildType('Debug') | 72 constants.SetBuildType('Debug') |
58 | 73 |
74 main_timer = time_profile.TimeProfile() | |
75 install_timer = time_profile.TimeProfile() | |
76 push_native_timer = time_profile.TimeProfile() | |
77 push_dex_timer = time_profile.TimeProfile() | |
78 | |
59 if args.device: | 79 if args.device: |
60 # Retries are annoying when commands fail for legitimate reasons. Might want | 80 # Retries are annoying when commands fail for legitimate reasons. Might want |
61 # to enable them if this is ever used on bots though. | 81 # to enable them if this is ever used on bots though. |
62 device = device_utils.DeviceUtils(args.device, default_retries=0) | 82 device = device_utils.DeviceUtils(args.device, default_retries=0) |
63 else: | 83 else: |
64 devices = device_utils.DeviceUtils.HealthyDevices(default_retries=0) | 84 devices = device_utils.DeviceUtils.HealthyDevices(default_retries=0) |
65 if not devices: | 85 if not devices: |
66 raise device_errors.NoDevicesError() | 86 raise device_errors.NoDevicesError() |
67 elif len(devices) == 1: | 87 elif len(devices) == 1: |
68 device = devices[0] | 88 device = devices[0] |
69 else: | 89 else: |
70 all_devices = device_utils.DeviceUtils.parallel(devices) | 90 all_devices = device_utils.DeviceUtils.parallel(devices) |
71 msg = ('More than one device available.\n' | 91 msg = ('More than one device available.\n' |
72 'Use --device=SERIAL to select a device.\n' | 92 'Use --device=SERIAL to select a device.\n' |
73 'Available devices:\n') | 93 'Available devices:\n') |
74 descriptions = all_devices.pMap(lambda d: d.build_description).pGet(None) | 94 descriptions = all_devices.pMap(lambda d: d.build_description).pGet(None) |
75 for d, desc in zip(devices, descriptions): | 95 for d, desc in zip(devices, descriptions): |
76 msg += ' %s (%s)\n' % (d, desc) | 96 msg += ' %s (%s)\n' % (d, desc) |
77 raise Exception(msg) | 97 raise Exception(msg) |
78 | 98 |
79 apk_package = apk_helper.ApkHelper(args.apk_path).GetPackageName() | 99 apk_help = apk_helper.ApkHelper(args.apk_path) |
100 apk_package = apk_help.GetPackageName() | |
80 device_incremental_dir = '/data/local/tmp/incremental-app-%s' % apk_package | 101 device_incremental_dir = '/data/local/tmp/incremental-app-%s' % apk_package |
81 | 102 |
82 if args.uninstall: | 103 if args.uninstall: |
83 logging.info('Uninstalling .apk') | 104 is_installed = device.GetApplicationPaths(apk_package) |
jbudorick
2015/09/15 18:36:41
Is this faster than just a raw Uninstall call? Tha
agrieve
2015/09/15 19:30:05
DeviceUtils.Uninstall() fails if the app isn't alr
| |
84 device.Uninstall(apk_package) | 105 if is_installed: |
106 logging.info('Uninstalling .apk') | |
107 device.Uninstall(apk_package) | |
85 logging.info('Removing side-loaded files') | 108 logging.info('Removing side-loaded files') |
86 device.RunShellCommand(['rm', '-rf', device_incremental_dir], | 109 device.RunShellCommand(['rm', '-rf', device_incremental_dir], |
87 check_return=True) | 110 check_return=True) |
111 logging.info('Uninstall took %s seconds.', main_timer.GetDelta()) | |
88 return | 112 return |
89 | 113 |
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.') | |
jbudorick
2015/09/15 18:36:41
o_o
yikes
agrieve
2015/09/15 19:30:05
yeah... :( maybe someone will figure it out, but f
| |
119 | |
90 # Install .apk(s) if any of them have changed. | 120 # Install .apk(s) if any of them have changed. |
91 def do_install(): | 121 def do_install(): |
122 install_timer.Start() | |
92 if args.splits: | 123 if args.splits: |
93 splits = [] | 124 splits = [] |
94 for split_glob in args.splits: | 125 for split_glob in args.splits: |
95 splits.extend((f for f in glob.glob(split_glob))) | 126 splits.extend((f for f in glob.glob(split_glob))) |
96 device.InstallSplitApk(args.apk_path, splits, reinstall=True, | 127 device.InstallSplitApk(args.apk_path, splits, reinstall=True, |
97 allow_cached_props=True) | 128 allow_cached_props=True) |
98 else: | 129 else: |
99 device.Install(args.apk_path, reinstall=True) | 130 device.Install(args.apk_path, reinstall=True) |
100 logging.info('Finished installing .apk') | 131 install_timer.Stop(log=False) |
101 | 132 |
102 # Push .so files to the device (if they have changed). | 133 # Push .so and .dex files to the device (if they have changed). |
103 def do_push_libs(): | 134 def do_push_files(): |
104 if args.lib_dir: | 135 if args.lib_dir: |
136 push_native_timer.Start() | |
105 device_lib_dir = posixpath.join(device_incremental_dir, 'lib') | 137 device_lib_dir = posixpath.join(device_incremental_dir, 'lib') |
106 device.PushChangedFiles([(args.lib_dir, device_lib_dir)], | 138 device.PushChangedFiles([(args.lib_dir, device_lib_dir)], |
107 delete_device_stale=True) | 139 delete_device_stale=True) |
108 logging.info('Finished pushing native libs') | 140 push_native_timer.Stop(log=False) |
109 | 141 |
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)) | |
jbudorick
2015/09/15 18:36:41
cross your fingers and hope for copy-on-write, I s
agrieve
2015/09/15 19:30:05
Yeah, might come back to this, but it's sub-second
| |
152 device.PushChangedFiles([(temp_dir, device_dex_dir)], | |
153 delete_device_stale=True) | |
154 push_dex_timer.Stop(log=False) | |
155 | |
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) | |
jbudorick
2015/09/15 18:36:41
I think I'm missing something... how is this creat
agrieve
2015/09/15 19:30:05
You're missing the invisible > that I changed too
| |
166 | |
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) | |
171 | |
172 create_lock_files() | |
110 # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't | 173 # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't |
111 # been designed for multi-threading. Enabling only because this is a | 174 # been designed for multi-threading. Enabling only because this is a |
112 # developer-only tool. | 175 # developer-only tool. |
113 if args.no_threading: | 176 if args.no_threading: |
114 do_install() | 177 do_install() |
115 do_push_libs() | 178 do_push_files() |
116 else: | 179 else: |
117 reraiser_thread.RunAsync((do_install, do_push_libs)) | 180 reraiser_thread.RunAsync((do_install, do_push_files)) |
118 logging.info('Took %s seconds', round(time.time() - start_time, 1)) | 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()) | |
119 | 185 |
120 | 186 |
121 if __name__ == '__main__': | 187 if __name__ == '__main__': |
122 sys.exit(main()) | 188 sys.exit(main()) |
123 | 189 |
OLD | NEW |