blob: d7b9c48d7f29cc963dcf794aded3daf86bc5f99d [file] [log] [blame]
Laszlo Csomorfe14baf2017-08-07 15:47:18 +02001# pylint: disable=g-direct-third-party-import
Laszlo Csomord4e673e2017-06-29 18:02:52 +02002# pylint: disable=g-bad-file-header
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00003# Copyright 2015 The Bazel Authors. All rights reserved.
Alex Humeskya4ecde62015-05-21 17:08:42 +00004#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Installs an Android application, possibly in an incremental way."""
18
19import collections
20import hashlib
21import logging
22import os
Laszlo Csomord4e673e2017-06-29 18:02:52 +020023import posixpath
Alex Humeskya4ecde62015-05-21 17:08:42 +000024import re
25import shutil
26import subprocess
27import sys
28import tempfile
29import time
30import zipfile
31
32from third_party.py import gflags
33from third_party.py.concurrent import futures
34
35
36gflags.DEFINE_string("split_main_apk", None, "The main APK for split install")
37gflags.DEFINE_multistring("split_apk", [], "Split APKs to install")
38gflags.DEFINE_string("dexmanifest", None, "The .dex manifest")
Lukacs Berkiecbf2422015-07-28 07:57:45 +000039gflags.DEFINE_multistring("native_lib", None, "Native libraries to install")
Alex Humeskya4ecde62015-05-21 17:08:42 +000040gflags.DEFINE_string("resource_apk", None, "The resource .apk")
41gflags.DEFINE_string("apk", None, "The app .apk. If not specified, "
42 "do incremental deployment")
43gflags.DEFINE_string("adb", None, "ADB to use")
44gflags.DEFINE_string("stub_datafile", None, "The stub data file")
45gflags.DEFINE_string("output_marker", None, "The output marker file")
46gflags.DEFINE_multistring("extra_adb_arg", [], "Extra arguments to adb")
47gflags.DEFINE_string("execroot", ".", "The exec root")
48gflags.DEFINE_integer("adb_jobs", 2,
49 "The number of instances of adb to use in parallel to "
50 "update files on the device",
51 lower_bound=1)
Googler2b88f622017-03-16 21:52:14 +000052gflags.DEFINE_enum("start", "no", ["no", "cold", "warm", "debug"],
53 "Whether/how to start the app after installing it. 'cold' "
54 "and 'warm' will both cause the app to be started, 'warm' "
55 "will start it with previously saved application state, "
56 "'debug' will wait for the debugger before a clean start.")
Googler73d4fc92015-07-30 20:39:46 +000057gflags.DEFINE_boolean("start_app", False, "Deprecated, use 'start'.")
Alex Humeskya4ecde62015-05-21 17:08:42 +000058gflags.DEFINE_string("user_home_dir", None, "Path to the user's home directory")
59gflags.DEFINE_string("flagfile", None,
60 "Path to a file to read additional flags from")
61gflags.DEFINE_string("verbosity", None, "Logging verbosity")
62
63FLAGS = gflags.FLAGS
64
65DEVICE_DIRECTORY = "/data/local/tmp/incrementaldeployment"
66
Lukacs Berkia3a33d72015-08-19 08:34:55 +000067# Some devices support ABIs other than those reported by getprop. In this case,
68# if the most specific ABI is not available in the .apk, we push the more
69# general ones.
70COMPATIBLE_ABIS = {
71 "armeabi-v7a": ["armeabi"],
72 "arm64-v8a": ["armeabi-v7a", "armeabi"]
73}
74
Alex Humeskya4ecde62015-05-21 17:08:42 +000075
76class AdbError(Exception):
77 """An exception class signaling an error in an adb invocation."""
78
79 def __init__(self, args, returncode, stdout, stderr):
80 self.args = args
81 self.returncode = returncode
82 self.stdout = stdout
83 self.stderr = stderr
84 details = "\n".join([
85 "adb command: %s" % args,
86 "return code: %s" % returncode,
87 "stdout: %s" % stdout,
88 "stderr: %s" % stderr,
89 ])
90 super(AdbError, self).__init__(details)
91
92
93class DeviceNotFoundError(Exception):
94 """Raised when the device could not be found."""
95
96
97class MultipleDevicesError(Exception):
98 """Raised when > 1 device is attached and no device serial was given."""
99
100 @staticmethod
101 def CheckError(s):
102 return re.search("more than one (device and emulator|device|emulator)", s)
103
104
105class DeviceUnauthorizedError(Exception):
106 """Raised when the local machine is not authorized to the device."""
107
108
109class TimestampException(Exception):
110 """Raised when there is a problem with timestamp reading/writing."""
111
112
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000113class OldSdkException(Exception):
114 """Raised when the SDK on the target device is older than the app allows."""
115
116
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200117class EnvvarError(Exception):
118 """Raised when a required environment variable is not set."""
119
120
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200121hostpath = os.path
122targetpath = posixpath
123
124
Alex Humeskya4ecde62015-05-21 17:08:42 +0000125class Adb(object):
126 """A class to handle interaction with adb."""
127
Akira Baruahe8a83af2017-12-13 08:33:11 -0800128 def __init__(self, adb_path, temp_dir, adb_jobs, user_home_dir,
129 extra_adb_args):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000130 self._adb_path = adb_path
131 self._temp_dir = temp_dir
132 self._user_home_dir = user_home_dir
133 self._file_counter = 1
134 self._executor = futures.ThreadPoolExecutor(max_workers=adb_jobs)
Akira Baruahe8a83af2017-12-13 08:33:11 -0800135 self._extra_adb_args = extra_adb_args or []
Alex Humeskya4ecde62015-05-21 17:08:42 +0000136
137 def _Exec(self, adb_args):
138 """Executes the given adb command + args."""
Akira Baruahe8a83af2017-12-13 08:33:11 -0800139 args = [self._adb_path] + self._extra_adb_args + adb_args
Alex Humeskya4ecde62015-05-21 17:08:42 +0000140 # TODO(ahumesky): Because multiple instances of adb are executed in
141 # parallel, these debug logging lines will get interleaved.
142 logging.debug("Executing: %s", " ".join(args))
143
144 # adb sometimes requires the user's home directory to access things in
145 # $HOME/.android (e.g. keys to authorize with the device). To avoid any
146 # potential problems with python picking up things in the user's home
147 # directory, HOME is not set in the environment around python and is instead
148 # passed explicitly as a flag.
149 env = {}
150 if self._user_home_dir:
151 env["HOME"] = self._user_home_dir
152
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200153 # On Windows, adb requires the SystemRoot environment variable.
154 if Adb._IsHostOsWindows():
155 value = os.getenv("SYSTEMROOT")
156 if not value:
157 raise EnvvarError(("The %SYSTEMROOT% environment variable must "
158 "be set or Adb won't work"))
159 env["SYSTEMROOT"] = value
160
Alex Humeskya4ecde62015-05-21 17:08:42 +0000161 adb = subprocess.Popen(
162 args,
163 stdin=subprocess.PIPE,
164 stdout=subprocess.PIPE,
165 stderr=subprocess.PIPE,
166 env=env)
167 stdout, stderr = adb.communicate()
168 stdout = stdout.strip()
169 stderr = stderr.strip()
170 logging.debug("adb ret: %s", adb.returncode)
171 logging.debug("adb out: %s", stdout)
172 logging.debug("adb err: %s", stderr)
173
174 # Check these first so that the more specific error gets raised instead of
175 # the more generic AdbError.
Laszlo Csomor00ac93a2019-03-03 23:30:52 -0800176 stdout = stdout.decode()
177 stderr = stderr.decode()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000178 if "device not found" in stderr:
179 raise DeviceNotFoundError()
180 elif "device unauthorized" in stderr:
181 raise DeviceUnauthorizedError()
182 elif MultipleDevicesError.CheckError(stderr):
183 # The error messages are from adb's transport.c, but something adds
184 # "error: " to the beginning, so take it off so that we don't end up
185 # printing "Error: error: ..."
186 raise MultipleDevicesError(re.sub("^error: ", "", stderr))
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000187 elif "INSTALL_FAILED_OLDER_SDK" in stdout:
188 raise OldSdkException()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000189
190 if adb.returncode != 0:
191 raise AdbError(args, adb.returncode, stdout, stderr)
192
193 return adb.returncode, stdout, stderr, args
194
195 def _ExecParallel(self, adb_args):
196 return self._executor.submit(self._Exec, adb_args)
197
198 def _CreateLocalFile(self):
199 """Returns a path to a temporary local file in the temp directory."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200200 local = hostpath.join(self._temp_dir, "adbfile_%d" % self._file_counter)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000201 self._file_counter += 1
202 return local
203
204 def GetInstallTime(self, package):
205 """Get the installation time of a package."""
206 _, stdout, _, _ = self._Shell("dumpsys package %s" % package)
Lukacs Berkifaff7182015-08-20 14:27:42 +0000207 match = re.search("firstInstallTime=(.*)$", stdout, re.MULTILINE)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000208 if match:
209 return match.group(1)
210 else:
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000211 return None
Alex Humeskya4ecde62015-05-21 17:08:42 +0000212
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000213 def GetAbi(self):
214 """Returns the ABI the device supports."""
215 _, stdout, _, _ = self._Shell("getprop ro.product.cpu.abi")
216 return stdout
217
Alex Humeskya4ecde62015-05-21 17:08:42 +0000218 def Push(self, local, remote):
219 """Invoke 'adb push' in parallel."""
220 return self._ExecParallel(["push", local, remote])
221
222 def PushString(self, contents, remote):
223 """Push a given string to a given path on the device in parallel."""
224 local = self._CreateLocalFile()
Laszlo Csomord456fca2017-08-10 10:21:53 +0200225 with open(local, "wb") as f:
Googler246ee562017-12-19 11:04:21 -0800226 f.write(contents.encode("utf-8"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000227 return self.Push(local, remote)
228
229 def Pull(self, remote):
230 """Invoke 'adb pull'.
231
232 Args:
233 remote: The path to the remote file to pull.
234
235 Returns:
236 The contents of a file or None if the file didn't exist.
237 """
238 local = self._CreateLocalFile()
239 try:
240 self._Exec(["pull", remote, local])
Laszlo Csomord456fca2017-08-10 10:21:53 +0200241 with open(local, "rb") as f:
Googler246ee562017-12-19 11:04:21 -0800242 return f.read().decode("utf-8")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000243 except (AdbError, IOError):
244 return None
245
246 def InstallMultiple(self, apk, pkg=None):
247 """Invoke 'adb install-multiple'."""
248
249 pkg_args = ["-p", pkg] if pkg else []
250 ret, stdout, stderr, args = self._Exec(
251 ["install-multiple", "-r"] + pkg_args + [apk])
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000252 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000253 raise AdbError(args, ret, stdout, stderr)
254
255 def Install(self, apk):
256 """Invoke 'adb install'."""
257 ret, stdout, stderr, args = self._Exec(["install", "-r", apk])
258
259 # adb install could fail with a message on stdout like this:
260 #
261 # pkg: /data/local/tmp/Gmail_dev_sharded_incremental.apk
262 # Failure [INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES]
263 #
264 # and yet it will still have a return code of 0. At least for the install
265 # command, it will print "Success" if it succeeded, so check for that in
266 # standard out instead of relying on the return code.
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000267 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000268 raise AdbError(args, ret, stdout, stderr)
269
Lukacs Berkifaff7182015-08-20 14:27:42 +0000270 def Uninstall(self, pkg):
271 """Invoke 'adb uninstall'."""
272 self._Exec(["uninstall", pkg])
273 # No error checking. If this fails, we assume that the app was not installed
274 # in the first place.
275
Alex Humeskya4ecde62015-05-21 17:08:42 +0000276 def Delete(self, remote):
277 """Delete the given file (or directory) on the device."""
278 self.DeleteMultiple([remote])
279
280 def DeleteMultiple(self, remote_files):
281 """Delete the given files (or directories) on the device."""
282 files_str = " ".join(remote_files)
283 if files_str:
284 self._Shell("rm -fr %s" % files_str)
285
286 def Mkdir(self, d):
287 """Invokes mkdir with the specified directory on the device."""
288 self._Shell("mkdir -p %s" % d)
289
290 def StopApp(self, package):
291 """Force stops the app with the given package."""
292 self._Shell("am force-stop %s" % package)
293
Googler73d4fc92015-07-30 20:39:46 +0000294 def StopAppAndSaveState(self, package):
295 """Stops the app with the given package, saving state for the next run."""
296 # 'am kill' will only kill processes in the background, so we must make sure
297 # our process is in the background first. We accomplish this by bringing up
298 # the app switcher.
299 self._Shell("input keyevent KEYCODE_APP_SWITCH")
300 self._Shell("am kill %s" % package)
301
Googler2b88f622017-03-16 21:52:14 +0000302 def StartApp(self, package, start_type):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000303 """Starts the app with the given package."""
Googler2b88f622017-03-16 21:52:14 +0000304 if start_type == "debug":
305 self._Shell("am set-debug-app -w --persistent %s" % package)
306 else:
307 self._Shell("am clear-debug-app %s" % package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000308 self._Shell("monkey -p %s -c android.intent.category.LAUNCHER 1" % package)
309
310 def _Shell(self, cmd):
311 """Invoke 'adb shell'."""
312 return self._Exec(["shell", cmd])
313
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200314 @staticmethod
315 def _IsHostOsWindows():
316 return os.name == "nt"
317
Alex Humeskya4ecde62015-05-21 17:08:42 +0000318
319ManifestEntry = collections.namedtuple(
320 "ManifestEntry", ["input_file", "zippath", "installpath", "sha256"])
321
322
323def ParseManifest(contents):
324 """Parses a dexmanifest file.
325
326 Args:
327 contents: the contents of the manifest file to be parsed.
328
329 Returns:
330 A dict of install path -> ManifestEntry.
331 """
332 result = {}
333
334 for l in contents.split("\n"):
335 entry = ManifestEntry(*(l.strip().split(" ")))
336 result[entry.installpath] = entry
337
338 return result
339
340
341def GetAppPackage(stub_datafile):
342 """Returns the app package specified in a stub data file."""
Laszlo Csomord456fca2017-08-10 10:21:53 +0200343 with open(stub_datafile, "rb") as f:
Googler246ee562017-12-19 11:04:21 -0800344 return f.readlines()[1].decode("utf-8").strip()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000345
346
347def UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, full_install):
348 """Uploads dexes to the device so that the state.
349
350 Does the minimum amount of work necessary to make the state of the device
351 consistent with what was built.
352
353 Args:
354 adb: the Adb instance representing the device to install to
355 execroot: the execroot
356 app_dir: the directory things should be installed under on the device
357 temp_dir: a local temporary directory
358 dexmanifest: contents of the dex manifest
359 full_install: whether to do a full install
360
361 Returns:
362 None.
363 """
364
365 # Fetch the manifest on the device
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200366 dex_dir = targetpath.join(app_dir, "dex")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000367 adb.Mkdir(dex_dir)
368
369 old_manifest = None
370
371 if not full_install:
372 logging.info("Fetching dex manifest from device...")
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200373 old_manifest_contents = adb.Pull(targetpath.join(dex_dir, "manifest"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000374 if old_manifest_contents:
375 old_manifest = ParseManifest(old_manifest_contents)
376 else:
377 logging.info("Dex manifest not found on device")
378
379 if old_manifest is None:
380 # If the manifest is not found, maybe a previous installation attempt
381 # was interrupted. Wipe the slate clean. Do this also in case we do a full
382 # installation.
383 old_manifest = {}
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200384 adb.Delete(targetpath.join(dex_dir, "*"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000385
386 new_manifest = ParseManifest(dexmanifest)
387 dexes_to_delete = set(old_manifest) - set(new_manifest)
388
389 # Figure out which dexes to upload: those that are present in the new manifest
390 # but not in the old one and those whose checksum was changed
391 common_dexes = set(new_manifest).intersection(old_manifest)
392 dexes_to_upload = set(d for d in common_dexes
393 if new_manifest[d].sha256 != old_manifest[d].sha256)
394 dexes_to_upload.update(set(new_manifest) - set(old_manifest))
395
396 if not dexes_to_delete and not dexes_to_upload:
397 # If we have nothing to do, don't bother removing and rewriting the manifest
398 logging.info("Application dexes up-to-date")
399 return
400
401 # Delete the manifest so that we know how to get back to a consistent state
402 # if we are interrupted.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200403 adb.Delete(targetpath.join(dex_dir, "manifest"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000404
405 # Tuple of (local, remote) files to push to the device.
406 files_to_push = []
407
408 # Sort dexes to be uploaded by the zip file they are in so that we only need
409 # to open each zip only once.
410 dexzips_in_upload = set(new_manifest[d].input_file for d in dexes_to_upload
411 if new_manifest[d].zippath != "-")
412 for i, dexzip_name in enumerate(dexzips_in_upload):
413 zip_dexes = [
414 d for d in dexes_to_upload if new_manifest[d].input_file == dexzip_name]
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200415 dexzip_tempdir = hostpath.join(temp_dir, "dex", str(i))
416 with zipfile.ZipFile(hostpath.join(execroot, dexzip_name)) as dexzip:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000417 for dex in zip_dexes:
418 zippath = new_manifest[dex].zippath
419 dexzip.extract(zippath, dexzip_tempdir)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200420 files_to_push.append((hostpath.join(dexzip_tempdir, zippath),
421 targetpath.join(dex_dir, dex)))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000422
423 # Now gather all the dexes that are not within a .zip file.
424 dexes_to_upload = set(
425 d for d in dexes_to_upload if new_manifest[d].zippath == "-")
426 for dex in dexes_to_upload:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200427 files_to_push.append((new_manifest[dex].input_file, targetpath.join(
428 dex_dir, dex)))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000429
430 num_files = len(dexes_to_delete) + len(files_to_push)
431 logging.info("Updating %d dex%s...", num_files, "es" if num_files > 1 else "")
432
433 # Delete the dexes that are not in the new manifest
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200434 adb.DeleteMultiple(targetpath.join(dex_dir, dex) for dex in dexes_to_delete)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000435
436 # Upload all the files.
437 upload_walltime_start = time.time()
438 fs = [adb.Push(local, remote) for local, remote in files_to_push]
439 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
440 upload_walltime = time.time() - upload_walltime_start
441 logging.debug("Dex upload walltime: %s seconds", upload_walltime)
442
443 # If there is anything in not_done, then some adb call failed and we
444 # can cancel the rest.
445 if not_done:
446 for f in not_done:
447 f.cancel()
448
449 # If any adb call resulted in an exception, re-raise it.
450 for f in done:
451 f.result()
452
453 # If no dex upload failed, upload the manifest. If any upload failed, the
454 # exception should have been re-raised above.
455 # Call result() to raise the exception if there was one.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200456 adb.PushString(dexmanifest, targetpath.join(dex_dir, "manifest")).result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000457
458
459def Checksum(filename):
460 """Compute the SHA-256 checksum of a file."""
461 h = hashlib.sha256()
Laszlo Csomord456fca2017-08-10 10:21:53 +0200462 with open(filename, "rb") as f:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000463 while True:
464 data = f.read(65536)
465 if not data:
466 break
467
468 h.update(data)
469
470 return h.hexdigest()
471
472
473def UploadResources(adb, resource_apk, app_dir):
474 """Uploads resources to the device.
475
476 Args:
477 adb: The Adb instance representing the device to install to.
478 resource_apk: Path to the resource apk.
479 app_dir: The directory things should be installed under on the device.
480
481 Returns:
482 None.
483 """
484
485 # Compute the checksum of the new resources file
486 new_checksum = Checksum(resource_apk)
487
488 # Fetch the checksum of the resources file on the device, if it exists
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200489 device_checksum_file = targetpath.join(app_dir, "resources_checksum")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000490 old_checksum = adb.Pull(device_checksum_file)
491 if old_checksum == new_checksum:
492 logging.info("Application resources up-to-date")
493 return
494 logging.info("Updating application resources...")
495
496 # Remove the checksum file on the device so that if the transfer is
497 # interrupted, we know how to get the device back to a consistent state.
498 adb.Delete(device_checksum_file)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200499 adb.Push(resource_apk, targetpath.join(app_dir, "resources.ap_")).result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000500
501 # Write the new checksum to the device.
502 adb.PushString(new_checksum, device_checksum_file).result()
503
504
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000505def ConvertNativeLibs(args):
506 """Converts the --native_libs command line argument to an arch -> libs map."""
507 native_libs = {}
508 if args is not None:
509 for native_lib in args:
510 abi, path = native_lib.split(":")
511 if abi not in native_libs:
512 native_libs[abi] = set()
513
514 native_libs[abi].add(path)
515
516 return native_libs
517
518
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000519def FindAbi(device_abi, app_abis):
520 """Selects which ABI native libs should be installed for."""
521 if device_abi in app_abis:
522 return device_abi
523
524 if device_abi in COMPATIBLE_ABIS:
525 for abi in COMPATIBLE_ABIS[device_abi]:
526 if abi in app_abis:
527 logging.warn("App does not have native libs for ABI '%s'. Using ABI "
528 "'%s'.", device_abi, abi)
529 return abi
530
531 logging.warn("No native libs for device ABI '%s'. App has native libs for "
532 "ABIs: %s", device_abi, ", ".join(app_abis))
533 return None
534
535
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000536def UploadNativeLibs(adb, native_lib_args, app_dir, full_install):
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000537 """Uploads native libraries to the device."""
538
539 native_libs = ConvertNativeLibs(native_lib_args)
540 libs = set()
541 if native_libs:
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000542 abi = FindAbi(adb.GetAbi(), native_libs.keys())
543 if abi:
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000544 libs = native_libs[abi]
545
546 basename_to_path = {}
547 install_checksums = {}
548 for lib in sorted(libs):
549 install_checksums[os.path.basename(lib)] = Checksum(lib)
550 basename_to_path[os.path.basename(lib)] = lib
551
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000552 device_manifest = None
553 if not full_install:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200554 device_manifest = adb.Pull(
555 targetpath.join(app_dir, "native", "native_manifest"))
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000556
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000557 device_checksums = {}
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000558 if device_manifest is None:
559 # If we couldn't fetch the device manifest or if this is a non-incremental
560 # install, wipe the slate clean
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200561 adb.Delete(targetpath.join(app_dir, "native"))
jingwenb3f28d32018-12-10 19:30:57 -0800562
563 # From Android 28 onwards, `adb push` creates directories with insufficient
564 # permissions, resulting in errors when pushing files. `adb shell mkdir`
565 # works correctly however, so we create the directory here.
566 # See https://github.com/bazelbuild/examples/issues/77 for more information.
567 adb.Mkdir(targetpath.join(app_dir, "native"))
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000568 else:
569 # Otherwise, parse the manifest. Note that this branch is also taken if the
570 # manifest is empty.
Lukacs Berkiff6ef9b2015-08-14 08:17:41 +0000571 for manifest_line in device_manifest.split("\n"):
572 if manifest_line:
573 name, checksum = manifest_line.split(" ")
574 device_checksums[name] = checksum
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000575
576 libs_to_delete = set(device_checksums) - set(install_checksums)
577 libs_to_upload = set(install_checksums) - set(device_checksums)
578 common_libs = set(install_checksums).intersection(set(device_checksums))
579 libs_to_upload.update([l for l in common_libs
580 if install_checksums[l] != device_checksums[l]])
581
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200582 libs_to_push = [(basename_to_path[lib], targetpath.join(
583 app_dir, "native", lib)) for lib in libs_to_upload]
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000584
585 if not libs_to_delete and not libs_to_push and device_manifest is not None:
586 logging.info("Native libs up-to-date")
587 return
588
589 num_files = len(libs_to_delete) + len(libs_to_push)
590 logging.info("Updating %d native lib%s...",
591 num_files, "s" if num_files != 1 else "")
592
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200593 adb.Delete(targetpath.join(app_dir, "native", "native_manifest"))
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000594
595 if libs_to_delete:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200596 adb.DeleteMultiple(
597 [targetpath.join(app_dir, "native", lib) for lib in libs_to_delete])
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000598
599 upload_walltime_start = time.time()
600 fs = [adb.Push(local, remote) for local, remote in libs_to_push]
601 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
602 upload_walltime = time.time() - upload_walltime_start
603 logging.debug("Native library upload walltime: %s seconds", upload_walltime)
604
605 # If there is anything in not_done, then some adb call failed and we
606 # can cancel the rest.
607 if not_done:
608 for f in not_done:
609 f.cancel()
610
611 # If any adb call resulted in an exception, re-raise it.
612 for f in done:
613 f.result()
614
615 install_manifest = [
Googler246ee562017-12-19 11:04:21 -0800616 name + " " + checksum for name, checksum in install_checksums.items()]
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000617 adb.PushString("\n".join(install_manifest),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200618 targetpath.join(app_dir, "native",
619 "native_manifest")).result()
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000620
621
Alex Humeskya4ecde62015-05-21 17:08:42 +0000622def VerifyInstallTimestamp(adb, app_package):
623 """Verifies that the app is unchanged since the last mobile-install."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200624 expected_timestamp = adb.Pull(
625 targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000626 if not expected_timestamp:
627 raise TimestampException(
628 "Cannot verify last mobile install. At least one non-incremental "
629 "'mobile-install' must precede incremental installs")
630
631 actual_timestamp = adb.GetInstallTime(app_package)
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000632 if actual_timestamp is None:
633 raise TimestampException(
634 "Package '%s' is not installed on the device. At least one "
635 "non-incremental 'mobile-install' must precede incremental "
636 "installs." % app_package)
637
Alex Humeskya4ecde62015-05-21 17:08:42 +0000638 if actual_timestamp != expected_timestamp:
639 raise TimestampException("Installed app '%s' has an unexpected timestamp. "
640 "Did you last install the app in a way other than "
641 "'mobile-install'?" % app_package)
642
643
Lukacs Berkifaff7182015-08-20 14:27:42 +0000644def SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
645 split_apks):
646 """Does incremental installation using split packages."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200647 app_dir = targetpath.join(DEVICE_DIRECTORY, app_package)
648 device_manifest_path = targetpath.join(app_dir, "split_manifest")
Lukacs Berkifaff7182015-08-20 14:27:42 +0000649 device_manifest = adb.Pull(device_manifest_path)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200650 expected_timestamp = adb.Pull(targetpath.join(app_dir, "install_timestamp"))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000651 actual_timestamp = adb.GetInstallTime(app_package)
652 device_checksums = {}
653 if device_manifest is not None:
654 for manifest_line in device_manifest.split("\n"):
655 if manifest_line:
656 name, checksum = manifest_line.split(" ")
657 device_checksums[name] = checksum
658
659 install_checksums = {}
660 install_checksums["__MAIN__"] = Checksum(
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200661 hostpath.join(execroot, split_main_apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000662 for apk in split_apks:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200663 install_checksums[apk] = Checksum(hostpath.join(execroot, apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000664
665 reinstall_main = False
666 if (device_manifest is None or actual_timestamp is None or
667 actual_timestamp != expected_timestamp or
668 install_checksums["__MAIN__"] != device_checksums["__MAIN__"] or
669 set(device_checksums.keys()) != set(install_checksums.keys())):
670 # The main app is not up to date or not present or something happened
671 # with the on-device manifest. Start from scratch. Notably, we cannot
672 # uninstall a split package, so if the set of packages changes, we also
673 # need to do a full reinstall.
674 reinstall_main = True
675 device_checksums = {}
676
677 apks_to_update = [
678 apk for apk in split_apks if
679 apk not in device_checksums or
680 device_checksums[apk] != install_checksums[apk]]
681
682 if not apks_to_update and not reinstall_main:
683 # Nothing to do
684 return
685
686 # Delete the device manifest so that if something goes wrong, we do a full
687 # reinstall next time
688 adb.Delete(device_manifest_path)
689
690 if reinstall_main:
691 logging.info("Installing main APK...")
692 adb.Uninstall(app_package)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200693 adb.InstallMultiple(targetpath.join(execroot, split_main_apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000694 adb.PushString(
695 adb.GetInstallTime(app_package),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200696 targetpath.join(app_dir, "install_timestamp")).result()
Lukacs Berkifaff7182015-08-20 14:27:42 +0000697
698 logging.info("Reinstalling %s APKs...", len(apks_to_update))
699
700 for apk in apks_to_update:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200701 adb.InstallMultiple(targetpath.join(execroot, apk), app_package)
Lukacs Berkifaff7182015-08-20 14:27:42 +0000702
703 install_manifest = [
Googler246ee562017-12-19 11:04:21 -0800704 name + " " + checksum for name, checksum in install_checksums.items()]
Lukacs Berkifaff7182015-08-20 14:27:42 +0000705 adb.PushString("\n".join(install_manifest),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200706 targetpath.join(app_dir, "split_manifest")).result()
Lukacs Berkifaff7182015-08-20 14:27:42 +0000707
708
Akira Baruahe8a83af2017-12-13 08:33:11 -0800709def IncrementalInstall(adb_path,
710 execroot,
711 stub_datafile,
712 output_marker,
713 adb_jobs,
714 start_type,
715 dexmanifest=None,
716 apk=None,
717 native_libs=None,
718 resource_apk=None,
719 split_main_apk=None,
720 split_apks=None,
721 user_home_dir=None,
722 extra_adb_args=None):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000723 """Performs an incremental install.
724
725 Args:
726 adb_path: Path to the adb executable.
727 execroot: Exec root.
728 stub_datafile: The stub datafile containing the app's package name.
729 output_marker: Path to the output marker file.
730 adb_jobs: The number of instances of adb to use in parallel.
Googler73d4fc92015-07-30 20:39:46 +0000731 start_type: A string describing whether/how to start the app after
732 installing it. Can be 'no', 'cold', or 'warm'.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000733 dexmanifest: Path to the .dex manifest file.
734 apk: Path to the .apk file. May be None to perform an incremental install.
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000735 native_libs: Native libraries to install.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000736 resource_apk: Path to the apk containing the app's resources.
737 split_main_apk: the split main .apk if split installation is desired.
738 split_apks: the list of split .apks to be installed.
739 user_home_dir: Path to the user's home directory.
Akira Baruahe8a83af2017-12-13 08:33:11 -0800740 extra_adb_args: Extra arguments that will always be passed to adb.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000741 """
742 temp_dir = tempfile.mkdtemp()
743 try:
Akira Baruahe8a83af2017-12-13 08:33:11 -0800744 adb = Adb(adb_path, temp_dir, adb_jobs, user_home_dir, extra_adb_args)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200745 app_package = GetAppPackage(hostpath.join(execroot, stub_datafile))
746 app_dir = targetpath.join(DEVICE_DIRECTORY, app_package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000747 if split_main_apk:
Lukacs Berkifaff7182015-08-20 14:27:42 +0000748 SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
749 split_apks)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000750 else:
751 if not apk:
752 VerifyInstallTimestamp(adb, app_package)
753
Laszlo Csomord456fca2017-08-10 10:21:53 +0200754 with open(hostpath.join(execroot, dexmanifest), "rb") as f:
Googler246ee562017-12-19 11:04:21 -0800755 dexmanifest = f.read().decode("utf-8")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000756 UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, bool(apk))
757 # TODO(ahumesky): UploadDexes waits for all the dexes to be uploaded, and
758 # then UploadResources is called. We could instead enqueue everything
759 # onto the threadpool so that uploading resources happens sooner.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200760 UploadResources(adb, hostpath.join(execroot, resource_apk), app_dir)
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000761 UploadNativeLibs(adb, native_libs, app_dir, bool(apk))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000762 if apk:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200763 apk_path = targetpath.join(execroot, apk)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000764 adb.Install(apk_path)
765 future = adb.PushString(
766 adb.GetInstallTime(app_package),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200767 targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000768 future.result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000769 else:
Googler73d4fc92015-07-30 20:39:46 +0000770 if start_type == "warm":
771 adb.StopAppAndSaveState(app_package)
772 else:
773 adb.StopApp(app_package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000774
Googler2b88f622017-03-16 21:52:14 +0000775 if start_type in ["cold", "warm", "debug"]:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000776 logging.info("Starting application %s", app_package)
Googler2b88f622017-03-16 21:52:14 +0000777 adb.StartApp(app_package, start_type)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000778
Laszlo Csomord456fca2017-08-10 10:21:53 +0200779 with open(output_marker, "wb") as _:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000780 pass
781 except DeviceNotFoundError:
782 sys.exit("Error: Device not found")
783 except DeviceUnauthorizedError:
784 sys.exit("Error: Device unauthorized. Please check the confirmation "
785 "dialog on your device.")
786 except MultipleDevicesError as e:
Googler246ee562017-12-19 11:04:21 -0800787 sys.exit("Error: " + str(e) + "\nTry specifying a device serial with "
Dan Fabulich1d35ca02018-07-05 16:08:06 -0700788 "\"bazel mobile-install --adb_arg=-s --adb_arg=$ANDROID_SERIAL\"")
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000789 except OldSdkException as e:
790 sys.exit("Error: The device does not support the API level specified in "
791 "the application's manifest. Check minSdkVersion in "
792 "AndroidManifest.xml")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000793 except TimestampException as e:
Googler246ee562017-12-19 11:04:21 -0800794 sys.exit("Error:\n%s" % str(e))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000795 except AdbError as e:
Googler246ee562017-12-19 11:04:21 -0800796 sys.exit("Error:\n%s" % str(e))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000797 finally:
798 shutil.rmtree(temp_dir, True)
799
800
801def main():
802 if FLAGS.verbosity == "1":
803 level = logging.DEBUG
804 fmt = "%(levelname)-5s %(asctime)s %(module)s:%(lineno)3d] %(message)s"
805 else:
806 level = logging.INFO
807 fmt = "%(message)s"
808 logging.basicConfig(stream=sys.stdout, level=level, format=fmt)
809
Googler73d4fc92015-07-30 20:39:46 +0000810 start_type = FLAGS.start
811 if FLAGS.start_app and start_type == "no":
812 start_type = "cold"
813
Alex Humeskya4ecde62015-05-21 17:08:42 +0000814 IncrementalInstall(
815 adb_path=FLAGS.adb,
816 adb_jobs=FLAGS.adb_jobs,
817 execroot=FLAGS.execroot,
818 stub_datafile=FLAGS.stub_datafile,
819 output_marker=FLAGS.output_marker,
Googler73d4fc92015-07-30 20:39:46 +0000820 start_type=start_type,
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000821 native_libs=FLAGS.native_lib,
Alex Humeskya4ecde62015-05-21 17:08:42 +0000822 split_main_apk=FLAGS.split_main_apk,
823 split_apks=FLAGS.split_apk,
824 dexmanifest=FLAGS.dexmanifest,
825 apk=FLAGS.apk,
826 resource_apk=FLAGS.resource_apk,
Akira Baruahe8a83af2017-12-13 08:33:11 -0800827 user_home_dir=FLAGS.user_home_dir,
828 extra_adb_args=FLAGS.extra_adb_arg)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000829
830
831if __name__ == "__main__":
832 FLAGS(sys.argv)
833 # process any additional flags in --flagfile
834 if FLAGS.flagfile:
Laszlo Csomord456fca2017-08-10 10:21:53 +0200835 with open(FLAGS.flagfile, "rb") as flagsfile:
Lukacs Berki682ad922015-06-11 08:01:07 +0000836 FLAGS.Reset()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000837 FLAGS(sys.argv + [line.strip() for line in flagsfile.readlines()])
Lukacs Berki682ad922015-06-11 08:01:07 +0000838
Alex Humeskya4ecde62015-05-21 17:08:42 +0000839 main()