blob: bef9246d090ebb441b9f2ae7220d9aefd23b8e07 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001# Copyright 2015 The Bazel Authors. All rights reserved.
Alex Humeskya4ecde62015-05-21 17:08:42 +00002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Installs an Android application, possibly in an incremental way."""
16
17import collections
18import hashlib
19import logging
20import os
21import re
22import shutil
23import subprocess
24import sys
25import tempfile
26import time
27import zipfile
28
29from third_party.py import gflags
30from third_party.py.concurrent import futures
31
32
33gflags.DEFINE_string("split_main_apk", None, "The main APK for split install")
34gflags.DEFINE_multistring("split_apk", [], "Split APKs to install")
35gflags.DEFINE_string("dexmanifest", None, "The .dex manifest")
Lukacs Berkiecbf2422015-07-28 07:57:45 +000036gflags.DEFINE_multistring("native_lib", None, "Native libraries to install")
Alex Humeskya4ecde62015-05-21 17:08:42 +000037gflags.DEFINE_string("resource_apk", None, "The resource .apk")
38gflags.DEFINE_string("apk", None, "The app .apk. If not specified, "
39 "do incremental deployment")
40gflags.DEFINE_string("adb", None, "ADB to use")
41gflags.DEFINE_string("stub_datafile", None, "The stub data file")
42gflags.DEFINE_string("output_marker", None, "The output marker file")
43gflags.DEFINE_multistring("extra_adb_arg", [], "Extra arguments to adb")
44gflags.DEFINE_string("execroot", ".", "The exec root")
45gflags.DEFINE_integer("adb_jobs", 2,
46 "The number of instances of adb to use in parallel to "
47 "update files on the device",
48 lower_bound=1)
Googler1c7e5812017-03-16 18:00:31 +000049gflags.DEFINE_enum("start", "no", ["no", "cold", "warm"], "Whether/how to "
50 "start the app after installing it. 'cold' and 'warm' will "
51 "both cause the app to be started, 'warm' will start it "
52 "with previously saved application state.")
Googler73d4fc92015-07-30 20:39:46 +000053gflags.DEFINE_boolean("start_app", False, "Deprecated, use 'start'.")
Alex Humeskya4ecde62015-05-21 17:08:42 +000054gflags.DEFINE_string("user_home_dir", None, "Path to the user's home directory")
55gflags.DEFINE_string("flagfile", None,
56 "Path to a file to read additional flags from")
57gflags.DEFINE_string("verbosity", None, "Logging verbosity")
Alex Humeskya4ecde62015-05-21 17:08:42 +000058
59FLAGS = gflags.FLAGS
60
61DEVICE_DIRECTORY = "/data/local/tmp/incrementaldeployment"
62
Lukacs Berkia3a33d72015-08-19 08:34:55 +000063# Some devices support ABIs other than those reported by getprop. In this case,
64# if the most specific ABI is not available in the .apk, we push the more
65# general ones.
66COMPATIBLE_ABIS = {
67 "armeabi-v7a": ["armeabi"],
68 "arm64-v8a": ["armeabi-v7a", "armeabi"]
69}
70
Alex Humeskya4ecde62015-05-21 17:08:42 +000071
72class AdbError(Exception):
73 """An exception class signaling an error in an adb invocation."""
74
75 def __init__(self, args, returncode, stdout, stderr):
76 self.args = args
77 self.returncode = returncode
78 self.stdout = stdout
79 self.stderr = stderr
80 details = "\n".join([
81 "adb command: %s" % args,
82 "return code: %s" % returncode,
83 "stdout: %s" % stdout,
84 "stderr: %s" % stderr,
85 ])
86 super(AdbError, self).__init__(details)
87
88
89class DeviceNotFoundError(Exception):
90 """Raised when the device could not be found."""
91
92
93class MultipleDevicesError(Exception):
94 """Raised when > 1 device is attached and no device serial was given."""
95
96 @staticmethod
97 def CheckError(s):
98 return re.search("more than one (device and emulator|device|emulator)", s)
99
100
101class DeviceUnauthorizedError(Exception):
102 """Raised when the local machine is not authorized to the device."""
103
104
105class TimestampException(Exception):
106 """Raised when there is a problem with timestamp reading/writing."""
107
108
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000109class OldSdkException(Exception):
110 """Raised when the SDK on the target device is older than the app allows."""
111
112
Alex Humeskya4ecde62015-05-21 17:08:42 +0000113class Adb(object):
114 """A class to handle interaction with adb."""
115
116 def __init__(self, adb_path, temp_dir, adb_jobs, user_home_dir):
117 self._adb_path = adb_path
118 self._temp_dir = temp_dir
119 self._user_home_dir = user_home_dir
120 self._file_counter = 1
121 self._executor = futures.ThreadPoolExecutor(max_workers=adb_jobs)
122
123 def _Exec(self, adb_args):
124 """Executes the given adb command + args."""
125 args = [self._adb_path] + FLAGS.extra_adb_arg + adb_args
126 # TODO(ahumesky): Because multiple instances of adb are executed in
127 # parallel, these debug logging lines will get interleaved.
128 logging.debug("Executing: %s", " ".join(args))
129
130 # adb sometimes requires the user's home directory to access things in
131 # $HOME/.android (e.g. keys to authorize with the device). To avoid any
132 # potential problems with python picking up things in the user's home
133 # directory, HOME is not set in the environment around python and is instead
134 # passed explicitly as a flag.
135 env = {}
136 if self._user_home_dir:
137 env["HOME"] = self._user_home_dir
138
139 adb = subprocess.Popen(
140 args,
141 stdin=subprocess.PIPE,
142 stdout=subprocess.PIPE,
143 stderr=subprocess.PIPE,
144 env=env)
145 stdout, stderr = adb.communicate()
146 stdout = stdout.strip()
147 stderr = stderr.strip()
148 logging.debug("adb ret: %s", adb.returncode)
149 logging.debug("adb out: %s", stdout)
150 logging.debug("adb err: %s", stderr)
151
152 # Check these first so that the more specific error gets raised instead of
153 # the more generic AdbError.
154 if "device not found" in stderr:
155 raise DeviceNotFoundError()
156 elif "device unauthorized" in stderr:
157 raise DeviceUnauthorizedError()
158 elif MultipleDevicesError.CheckError(stderr):
159 # The error messages are from adb's transport.c, but something adds
160 # "error: " to the beginning, so take it off so that we don't end up
161 # printing "Error: error: ..."
162 raise MultipleDevicesError(re.sub("^error: ", "", stderr))
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000163 elif "INSTALL_FAILED_OLDER_SDK" in stdout:
164 raise OldSdkException()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000165
166 if adb.returncode != 0:
167 raise AdbError(args, adb.returncode, stdout, stderr)
168
169 return adb.returncode, stdout, stderr, args
170
171 def _ExecParallel(self, adb_args):
172 return self._executor.submit(self._Exec, adb_args)
173
174 def _CreateLocalFile(self):
175 """Returns a path to a temporary local file in the temp directory."""
176 local = os.path.join(self._temp_dir, "adbfile_%d" % self._file_counter)
177 self._file_counter += 1
178 return local
179
180 def GetInstallTime(self, package):
181 """Get the installation time of a package."""
182 _, stdout, _, _ = self._Shell("dumpsys package %s" % package)
Lukacs Berkifaff7182015-08-20 14:27:42 +0000183 match = re.search("firstInstallTime=(.*)$", stdout, re.MULTILINE)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000184 if match:
185 return match.group(1)
186 else:
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000187 return None
Alex Humeskya4ecde62015-05-21 17:08:42 +0000188
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000189 def GetAbi(self):
190 """Returns the ABI the device supports."""
191 _, stdout, _, _ = self._Shell("getprop ro.product.cpu.abi")
192 return stdout
193
Alex Humeskya4ecde62015-05-21 17:08:42 +0000194 def Push(self, local, remote):
195 """Invoke 'adb push' in parallel."""
196 return self._ExecParallel(["push", local, remote])
197
198 def PushString(self, contents, remote):
199 """Push a given string to a given path on the device in parallel."""
200 local = self._CreateLocalFile()
201 with file(local, "w") as f:
202 f.write(contents)
203 return self.Push(local, remote)
204
205 def Pull(self, remote):
206 """Invoke 'adb pull'.
207
208 Args:
209 remote: The path to the remote file to pull.
210
211 Returns:
212 The contents of a file or None if the file didn't exist.
213 """
214 local = self._CreateLocalFile()
215 try:
216 self._Exec(["pull", remote, local])
217 with file(local) as f:
218 return f.read()
219 except (AdbError, IOError):
220 return None
221
222 def InstallMultiple(self, apk, pkg=None):
223 """Invoke 'adb install-multiple'."""
224
225 pkg_args = ["-p", pkg] if pkg else []
226 ret, stdout, stderr, args = self._Exec(
227 ["install-multiple", "-r"] + pkg_args + [apk])
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000228 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000229 raise AdbError(args, ret, stdout, stderr)
230
231 def Install(self, apk):
232 """Invoke 'adb install'."""
233 ret, stdout, stderr, args = self._Exec(["install", "-r", apk])
234
235 # adb install could fail with a message on stdout like this:
236 #
237 # pkg: /data/local/tmp/Gmail_dev_sharded_incremental.apk
238 # Failure [INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES]
239 #
240 # and yet it will still have a return code of 0. At least for the install
241 # command, it will print "Success" if it succeeded, so check for that in
242 # standard out instead of relying on the return code.
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000243 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000244 raise AdbError(args, ret, stdout, stderr)
245
Lukacs Berkifaff7182015-08-20 14:27:42 +0000246 def Uninstall(self, pkg):
247 """Invoke 'adb uninstall'."""
248 self._Exec(["uninstall", pkg])
249 # No error checking. If this fails, we assume that the app was not installed
250 # in the first place.
251
Alex Humeskya4ecde62015-05-21 17:08:42 +0000252 def Delete(self, remote):
253 """Delete the given file (or directory) on the device."""
254 self.DeleteMultiple([remote])
255
256 def DeleteMultiple(self, remote_files):
257 """Delete the given files (or directories) on the device."""
258 files_str = " ".join(remote_files)
259 if files_str:
260 self._Shell("rm -fr %s" % files_str)
261
262 def Mkdir(self, d):
263 """Invokes mkdir with the specified directory on the device."""
264 self._Shell("mkdir -p %s" % d)
265
266 def StopApp(self, package):
267 """Force stops the app with the given package."""
268 self._Shell("am force-stop %s" % package)
269
Googler73d4fc92015-07-30 20:39:46 +0000270 def StopAppAndSaveState(self, package):
271 """Stops the app with the given package, saving state for the next run."""
272 # 'am kill' will only kill processes in the background, so we must make sure
273 # our process is in the background first. We accomplish this by bringing up
274 # the app switcher.
275 self._Shell("input keyevent KEYCODE_APP_SWITCH")
276 self._Shell("am kill %s" % package)
277
Alex Humeskya4ecde62015-05-21 17:08:42 +0000278 def StartApp(self, package):
279 """Starts the app with the given package."""
280 self._Shell("monkey -p %s -c android.intent.category.LAUNCHER 1" % package)
281
282 def _Shell(self, cmd):
283 """Invoke 'adb shell'."""
284 return self._Exec(["shell", cmd])
285
286
287ManifestEntry = collections.namedtuple(
288 "ManifestEntry", ["input_file", "zippath", "installpath", "sha256"])
289
290
291def ParseManifest(contents):
292 """Parses a dexmanifest file.
293
294 Args:
295 contents: the contents of the manifest file to be parsed.
296
297 Returns:
298 A dict of install path -> ManifestEntry.
299 """
300 result = {}
301
302 for l in contents.split("\n"):
303 entry = ManifestEntry(*(l.strip().split(" ")))
304 result[entry.installpath] = entry
305
306 return result
307
308
309def GetAppPackage(stub_datafile):
310 """Returns the app package specified in a stub data file."""
311 with file(stub_datafile) as f:
312 return f.readlines()[1].strip()
313
314
315def UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, full_install):
316 """Uploads dexes to the device so that the state.
317
318 Does the minimum amount of work necessary to make the state of the device
319 consistent with what was built.
320
321 Args:
322 adb: the Adb instance representing the device to install to
323 execroot: the execroot
324 app_dir: the directory things should be installed under on the device
325 temp_dir: a local temporary directory
326 dexmanifest: contents of the dex manifest
327 full_install: whether to do a full install
328
329 Returns:
330 None.
331 """
332
333 # Fetch the manifest on the device
334 dex_dir = os.path.join(app_dir, "dex")
335 adb.Mkdir(dex_dir)
336
337 old_manifest = None
338
339 if not full_install:
340 logging.info("Fetching dex manifest from device...")
341 old_manifest_contents = adb.Pull("%s/manifest" % dex_dir)
342 if old_manifest_contents:
343 old_manifest = ParseManifest(old_manifest_contents)
344 else:
345 logging.info("Dex manifest not found on device")
346
347 if old_manifest is None:
348 # If the manifest is not found, maybe a previous installation attempt
349 # was interrupted. Wipe the slate clean. Do this also in case we do a full
350 # installation.
351 old_manifest = {}
352 adb.Delete("%s/*" % dex_dir)
353
354 new_manifest = ParseManifest(dexmanifest)
355 dexes_to_delete = set(old_manifest) - set(new_manifest)
356
357 # Figure out which dexes to upload: those that are present in the new manifest
358 # but not in the old one and those whose checksum was changed
359 common_dexes = set(new_manifest).intersection(old_manifest)
360 dexes_to_upload = set(d for d in common_dexes
361 if new_manifest[d].sha256 != old_manifest[d].sha256)
362 dexes_to_upload.update(set(new_manifest) - set(old_manifest))
363
364 if not dexes_to_delete and not dexes_to_upload:
365 # If we have nothing to do, don't bother removing and rewriting the manifest
366 logging.info("Application dexes up-to-date")
367 return
368
369 # Delete the manifest so that we know how to get back to a consistent state
370 # if we are interrupted.
371 adb.Delete("%s/manifest" % dex_dir)
372
373 # Tuple of (local, remote) files to push to the device.
374 files_to_push = []
375
376 # Sort dexes to be uploaded by the zip file they are in so that we only need
377 # to open each zip only once.
378 dexzips_in_upload = set(new_manifest[d].input_file for d in dexes_to_upload
379 if new_manifest[d].zippath != "-")
380 for i, dexzip_name in enumerate(dexzips_in_upload):
381 zip_dexes = [
382 d for d in dexes_to_upload if new_manifest[d].input_file == dexzip_name]
383 dexzip_tempdir = os.path.join(temp_dir, "dex", str(i))
384 with zipfile.ZipFile(os.path.join(execroot, dexzip_name)) as dexzip:
385 for dex in zip_dexes:
386 zippath = new_manifest[dex].zippath
387 dexzip.extract(zippath, dexzip_tempdir)
388 files_to_push.append(
389 (os.path.join(dexzip_tempdir, zippath), "%s/%s" % (dex_dir, dex)))
390
391 # Now gather all the dexes that are not within a .zip file.
392 dexes_to_upload = set(
393 d for d in dexes_to_upload if new_manifest[d].zippath == "-")
394 for dex in dexes_to_upload:
395 files_to_push.append(
396 (new_manifest[dex].input_file, "%s/%s" % (dex_dir, dex)))
397
398 num_files = len(dexes_to_delete) + len(files_to_push)
399 logging.info("Updating %d dex%s...", num_files, "es" if num_files > 1 else "")
400
401 # Delete the dexes that are not in the new manifest
402 adb.DeleteMultiple(os.path.join(dex_dir, dex) for dex in dexes_to_delete)
403
404 # Upload all the files.
405 upload_walltime_start = time.time()
406 fs = [adb.Push(local, remote) for local, remote in files_to_push]
407 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
408 upload_walltime = time.time() - upload_walltime_start
409 logging.debug("Dex upload walltime: %s seconds", upload_walltime)
410
411 # If there is anything in not_done, then some adb call failed and we
412 # can cancel the rest.
413 if not_done:
414 for f in not_done:
415 f.cancel()
416
417 # If any adb call resulted in an exception, re-raise it.
418 for f in done:
419 f.result()
420
421 # If no dex upload failed, upload the manifest. If any upload failed, the
422 # exception should have been re-raised above.
423 # Call result() to raise the exception if there was one.
424 adb.PushString(dexmanifest, "%s/manifest" % dex_dir).result()
425
426
427def Checksum(filename):
428 """Compute the SHA-256 checksum of a file."""
429 h = hashlib.sha256()
430 with file(filename, "r") as f:
431 while True:
432 data = f.read(65536)
433 if not data:
434 break
435
436 h.update(data)
437
438 return h.hexdigest()
439
440
441def UploadResources(adb, resource_apk, app_dir):
442 """Uploads resources to the device.
443
444 Args:
445 adb: The Adb instance representing the device to install to.
446 resource_apk: Path to the resource apk.
447 app_dir: The directory things should be installed under on the device.
448
449 Returns:
450 None.
451 """
452
453 # Compute the checksum of the new resources file
454 new_checksum = Checksum(resource_apk)
455
456 # Fetch the checksum of the resources file on the device, if it exists
457 device_checksum_file = "%s/%s" % (app_dir, "resources_checksum")
458 old_checksum = adb.Pull(device_checksum_file)
459 if old_checksum == new_checksum:
460 logging.info("Application resources up-to-date")
461 return
462 logging.info("Updating application resources...")
463
464 # Remove the checksum file on the device so that if the transfer is
465 # interrupted, we know how to get the device back to a consistent state.
466 adb.Delete(device_checksum_file)
467 adb.Push(resource_apk, "%s/%s" % (app_dir, "resources.ap_")).result()
468
469 # Write the new checksum to the device.
470 adb.PushString(new_checksum, device_checksum_file).result()
471
472
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000473def ConvertNativeLibs(args):
474 """Converts the --native_libs command line argument to an arch -> libs map."""
475 native_libs = {}
476 if args is not None:
477 for native_lib in args:
478 abi, path = native_lib.split(":")
479 if abi not in native_libs:
480 native_libs[abi] = set()
481
482 native_libs[abi].add(path)
483
484 return native_libs
485
486
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000487def FindAbi(device_abi, app_abis):
488 """Selects which ABI native libs should be installed for."""
489 if device_abi in app_abis:
490 return device_abi
491
492 if device_abi in COMPATIBLE_ABIS:
493 for abi in COMPATIBLE_ABIS[device_abi]:
494 if abi in app_abis:
495 logging.warn("App does not have native libs for ABI '%s'. Using ABI "
496 "'%s'.", device_abi, abi)
497 return abi
498
499 logging.warn("No native libs for device ABI '%s'. App has native libs for "
500 "ABIs: %s", device_abi, ", ".join(app_abis))
501 return None
502
503
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000504def UploadNativeLibs(adb, native_lib_args, app_dir, full_install):
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000505 """Uploads native libraries to the device."""
506
507 native_libs = ConvertNativeLibs(native_lib_args)
508 libs = set()
509 if native_libs:
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000510 abi = FindAbi(adb.GetAbi(), native_libs.keys())
511 if abi:
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000512 libs = native_libs[abi]
513
514 basename_to_path = {}
515 install_checksums = {}
516 for lib in sorted(libs):
517 install_checksums[os.path.basename(lib)] = Checksum(lib)
518 basename_to_path[os.path.basename(lib)] = lib
519
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000520 device_manifest = None
521 if not full_install:
522 device_manifest = adb.Pull("%s/native/native_manifest" % app_dir)
523
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000524 device_checksums = {}
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000525 if device_manifest is None:
526 # If we couldn't fetch the device manifest or if this is a non-incremental
527 # install, wipe the slate clean
528 adb.Delete("%s/native" % app_dir)
529 else:
530 # Otherwise, parse the manifest. Note that this branch is also taken if the
531 # manifest is empty.
Lukacs Berkiff6ef9b2015-08-14 08:17:41 +0000532 for manifest_line in device_manifest.split("\n"):
533 if manifest_line:
534 name, checksum = manifest_line.split(" ")
535 device_checksums[name] = checksum
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000536
537 libs_to_delete = set(device_checksums) - set(install_checksums)
538 libs_to_upload = set(install_checksums) - set(device_checksums)
539 common_libs = set(install_checksums).intersection(set(device_checksums))
540 libs_to_upload.update([l for l in common_libs
541 if install_checksums[l] != device_checksums[l]])
542
543 libs_to_push = [(basename_to_path[lib], "%s/native/%s" % (app_dir, lib))
544 for lib in libs_to_upload]
545
546 if not libs_to_delete and not libs_to_push and device_manifest is not None:
547 logging.info("Native libs up-to-date")
548 return
549
550 num_files = len(libs_to_delete) + len(libs_to_push)
551 logging.info("Updating %d native lib%s...",
552 num_files, "s" if num_files != 1 else "")
553
554 adb.Delete("%s/native/native_manifest" % app_dir)
555
556 if libs_to_delete:
557 adb.DeleteMultiple([
558 "%s/native/%s" % (app_dir, lib) for lib in libs_to_delete])
559
560 upload_walltime_start = time.time()
561 fs = [adb.Push(local, remote) for local, remote in libs_to_push]
562 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
563 upload_walltime = time.time() - upload_walltime_start
564 logging.debug("Native library upload walltime: %s seconds", upload_walltime)
565
566 # If there is anything in not_done, then some adb call failed and we
567 # can cancel the rest.
568 if not_done:
569 for f in not_done:
570 f.cancel()
571
572 # If any adb call resulted in an exception, re-raise it.
573 for f in done:
574 f.result()
575
576 install_manifest = [
577 name + " " + checksum for name, checksum in install_checksums.iteritems()]
578 adb.PushString("\n".join(install_manifest),
579 "%s/native/native_manifest" % app_dir).result()
580
581
Alex Humeskya4ecde62015-05-21 17:08:42 +0000582def VerifyInstallTimestamp(adb, app_package):
583 """Verifies that the app is unchanged since the last mobile-install."""
584 expected_timestamp = adb.Pull("%s/%s/install_timestamp" % (
585 DEVICE_DIRECTORY, app_package))
586 if not expected_timestamp:
587 raise TimestampException(
588 "Cannot verify last mobile install. At least one non-incremental "
589 "'mobile-install' must precede incremental installs")
590
591 actual_timestamp = adb.GetInstallTime(app_package)
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000592 if actual_timestamp is None:
593 raise TimestampException(
594 "Package '%s' is not installed on the device. At least one "
595 "non-incremental 'mobile-install' must precede incremental "
596 "installs." % app_package)
597
Alex Humeskya4ecde62015-05-21 17:08:42 +0000598 if actual_timestamp != expected_timestamp:
599 raise TimestampException("Installed app '%s' has an unexpected timestamp. "
600 "Did you last install the app in a way other than "
601 "'mobile-install'?" % app_package)
602
603
Lukacs Berkifaff7182015-08-20 14:27:42 +0000604def SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
605 split_apks):
606 """Does incremental installation using split packages."""
607 app_dir = os.path.join(DEVICE_DIRECTORY, app_package)
608 device_manifest_path = "%s/split_manifest" % app_dir
609 device_manifest = adb.Pull(device_manifest_path)
610 expected_timestamp = adb.Pull("%s/install_timestamp" % app_dir)
611 actual_timestamp = adb.GetInstallTime(app_package)
612 device_checksums = {}
613 if device_manifest is not None:
614 for manifest_line in device_manifest.split("\n"):
615 if manifest_line:
616 name, checksum = manifest_line.split(" ")
617 device_checksums[name] = checksum
618
619 install_checksums = {}
620 install_checksums["__MAIN__"] = Checksum(
621 os.path.join(execroot, split_main_apk))
622 for apk in split_apks:
623 install_checksums[apk] = Checksum(os.path.join(execroot, apk))
624
625 reinstall_main = False
626 if (device_manifest is None or actual_timestamp is None or
627 actual_timestamp != expected_timestamp or
628 install_checksums["__MAIN__"] != device_checksums["__MAIN__"] or
629 set(device_checksums.keys()) != set(install_checksums.keys())):
630 # The main app is not up to date or not present or something happened
631 # with the on-device manifest. Start from scratch. Notably, we cannot
632 # uninstall a split package, so if the set of packages changes, we also
633 # need to do a full reinstall.
634 reinstall_main = True
635 device_checksums = {}
636
637 apks_to_update = [
638 apk for apk in split_apks if
639 apk not in device_checksums or
640 device_checksums[apk] != install_checksums[apk]]
641
642 if not apks_to_update and not reinstall_main:
643 # Nothing to do
644 return
645
646 # Delete the device manifest so that if something goes wrong, we do a full
647 # reinstall next time
648 adb.Delete(device_manifest_path)
649
650 if reinstall_main:
651 logging.info("Installing main APK...")
652 adb.Uninstall(app_package)
653 adb.InstallMultiple(os.path.join(execroot, split_main_apk))
654 adb.PushString(
655 adb.GetInstallTime(app_package),
656 "%s/install_timestamp" % app_dir).result()
657
658 logging.info("Reinstalling %s APKs...", len(apks_to_update))
659
660 for apk in apks_to_update:
661 adb.InstallMultiple(os.path.join(execroot, apk), app_package)
662
663 install_manifest = [
664 name + " " + checksum for name, checksum in install_checksums.iteritems()]
665 adb.PushString("\n".join(install_manifest),
666 "%s/split_manifest" % app_dir).result()
667
668
Alex Humeskya4ecde62015-05-21 17:08:42 +0000669def IncrementalInstall(adb_path, execroot, stub_datafile, output_marker,
Googler73d4fc92015-07-30 20:39:46 +0000670 adb_jobs, start_type, dexmanifest=None, apk=None,
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000671 native_libs=None, resource_apk=None,
672 split_main_apk=None, split_apks=None,
Alex Humeskya4ecde62015-05-21 17:08:42 +0000673 user_home_dir=None):
674 """Performs an incremental install.
675
676 Args:
677 adb_path: Path to the adb executable.
678 execroot: Exec root.
679 stub_datafile: The stub datafile containing the app's package name.
680 output_marker: Path to the output marker file.
681 adb_jobs: The number of instances of adb to use in parallel.
Googler73d4fc92015-07-30 20:39:46 +0000682 start_type: A string describing whether/how to start the app after
683 installing it. Can be 'no', 'cold', or 'warm'.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000684 dexmanifest: Path to the .dex manifest file.
685 apk: Path to the .apk file. May be None to perform an incremental install.
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000686 native_libs: Native libraries to install.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000687 resource_apk: Path to the apk containing the app's resources.
688 split_main_apk: the split main .apk if split installation is desired.
689 split_apks: the list of split .apks to be installed.
690 user_home_dir: Path to the user's home directory.
691 """
692 temp_dir = tempfile.mkdtemp()
693 try:
694 adb = Adb(adb_path, temp_dir, adb_jobs, user_home_dir)
695 app_package = GetAppPackage(os.path.join(execroot, stub_datafile))
696 app_dir = os.path.join(DEVICE_DIRECTORY, app_package)
697 if split_main_apk:
Lukacs Berkifaff7182015-08-20 14:27:42 +0000698 SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
699 split_apks)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000700 else:
701 if not apk:
702 VerifyInstallTimestamp(adb, app_package)
703
704 with file(os.path.join(execroot, dexmanifest)) as f:
705 dexmanifest = f.read()
706 UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, bool(apk))
707 # TODO(ahumesky): UploadDexes waits for all the dexes to be uploaded, and
708 # then UploadResources is called. We could instead enqueue everything
709 # onto the threadpool so that uploading resources happens sooner.
710 UploadResources(adb, os.path.join(execroot, resource_apk), app_dir)
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000711 UploadNativeLibs(adb, native_libs, app_dir, bool(apk))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000712 if apk:
713 apk_path = os.path.join(execroot, apk)
714 adb.Install(apk_path)
715 future = adb.PushString(
716 adb.GetInstallTime(app_package),
717 "%s/%s/install_timestamp" % (DEVICE_DIRECTORY, app_package))
718 future.result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000719 else:
Googler73d4fc92015-07-30 20:39:46 +0000720 if start_type == "warm":
721 adb.StopAppAndSaveState(app_package)
722 else:
723 adb.StopApp(app_package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000724
Googler73d4fc92015-07-30 20:39:46 +0000725 if start_type in ["cold", "warm"]:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000726 logging.info("Starting application %s", app_package)
727 adb.StartApp(app_package)
728
729 with file(output_marker, "w") as _:
730 pass
731 except DeviceNotFoundError:
732 sys.exit("Error: Device not found")
733 except DeviceUnauthorizedError:
734 sys.exit("Error: Device unauthorized. Please check the confirmation "
735 "dialog on your device.")
736 except MultipleDevicesError as e:
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000737 sys.exit("Error: " + e.message + "\nTry specifying a device serial with "
738 "\"blaze mobile-install --adb_arg=-s --adb_arg=$ANDROID_SERIAL\"")
739 except OldSdkException as e:
740 sys.exit("Error: The device does not support the API level specified in "
741 "the application's manifest. Check minSdkVersion in "
742 "AndroidManifest.xml")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000743 except TimestampException as e:
744 sys.exit("Error:\n%s" % e.message)
745 except AdbError as e:
746 sys.exit("Error:\n%s" % e.message)
747 finally:
748 shutil.rmtree(temp_dir, True)
749
750
751def main():
752 if FLAGS.verbosity == "1":
753 level = logging.DEBUG
754 fmt = "%(levelname)-5s %(asctime)s %(module)s:%(lineno)3d] %(message)s"
755 else:
756 level = logging.INFO
757 fmt = "%(message)s"
758 logging.basicConfig(stream=sys.stdout, level=level, format=fmt)
759
Googler73d4fc92015-07-30 20:39:46 +0000760 start_type = FLAGS.start
761 if FLAGS.start_app and start_type == "no":
762 start_type = "cold"
763
Alex Humeskya4ecde62015-05-21 17:08:42 +0000764 IncrementalInstall(
765 adb_path=FLAGS.adb,
766 adb_jobs=FLAGS.adb_jobs,
767 execroot=FLAGS.execroot,
768 stub_datafile=FLAGS.stub_datafile,
769 output_marker=FLAGS.output_marker,
Googler73d4fc92015-07-30 20:39:46 +0000770 start_type=start_type,
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000771 native_libs=FLAGS.native_lib,
Alex Humeskya4ecde62015-05-21 17:08:42 +0000772 split_main_apk=FLAGS.split_main_apk,
773 split_apks=FLAGS.split_apk,
774 dexmanifest=FLAGS.dexmanifest,
775 apk=FLAGS.apk,
776 resource_apk=FLAGS.resource_apk,
777 user_home_dir=FLAGS.user_home_dir)
778
779
780if __name__ == "__main__":
781 FLAGS(sys.argv)
782 # process any additional flags in --flagfile
783 if FLAGS.flagfile:
784 with open(FLAGS.flagfile) as flagsfile:
Lukacs Berki682ad922015-06-11 08:01:07 +0000785 FLAGS.Reset()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000786 FLAGS(sys.argv + [line.strip() for line in flagsfile.readlines()])
Lukacs Berki682ad922015-06-11 08:01:07 +0000787
Alex Humeskya4ecde62015-05-21 17:08:42 +0000788 main()