blob: c288cd427ffc1f33b1c65719630036b98898b761 [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
laszlocsomord93a1462019-11-04 09:14:39 -080020from concurrent import futures
Alex Humeskya4ecde62015-05-21 17:08:42 +000021import hashlib
22import logging
23import os
Laszlo Csomord4e673e2017-06-29 18:02:52 +020024import posixpath
Alex Humeskya4ecde62015-05-21 17:08:42 +000025import re
26import shutil
27import subprocess
28import sys
29import tempfile
30import time
31import zipfile
32
laszlocsomord93a1462019-11-04 09:14:39 -080033# Do not edit this line. Copybara replaces it with PY2 migration helper.
34from absl import app
35from absl import flags
Alex Humeskya4ecde62015-05-21 17:08:42 +000036
laszlocsomord93a1462019-11-04 09:14:39 -080037flags.DEFINE_string("split_main_apk", None, "The main APK for split install")
38flags.DEFINE_multi_string("split_apk", [], "Split APKs to install")
39flags.DEFINE_string("dexmanifest", None, "The .dex manifest")
40flags.DEFINE_multi_string("native_lib", None, "Native libraries to install")
41flags.DEFINE_string("resource_apk", None, "The resource .apk")
42flags.DEFINE_string(
43 "apk", None, "The app .apk. If not specified, "
44 "do incremental deployment")
45flags.DEFINE_string("adb", None, "ADB to use")
46flags.DEFINE_string("stub_datafile", None, "The stub data file")
47flags.DEFINE_string("output_marker", None, "The output marker file")
48flags.DEFINE_multi_string("extra_adb_arg", [], "Extra arguments to adb")
49flags.DEFINE_string("execroot", ".", "The exec root")
50flags.DEFINE_integer(
51 "adb_jobs",
52 2, "The number of instances of adb to use in parallel to "
53 "update files on the device",
54 lower_bound=1)
55flags.DEFINE_enum(
56 "start", "no", ["no", "cold", "warm", "debug"],
57 "Whether/how to start the app after installing it. 'cold' "
58 "and 'warm' will both cause the app to be started, 'warm' "
59 "will start it with previously saved application state, "
60 "'debug' will wait for the debugger before a clean start.")
61flags.DEFINE_boolean("start_app", False, "Deprecated, use 'start'.")
62flags.DEFINE_string("user_home_dir", None, "Path to the user's home directory")
63flags.DEFINE_string("flagfile", None,
64 "Path to a file to read additional flags from")
Alex Humeskya4ecde62015-05-21 17:08:42 +000065
laszlocsomord93a1462019-11-04 09:14:39 -080066FLAGS = flags.FLAGS
Alex Humeskya4ecde62015-05-21 17:08:42 +000067
68DEVICE_DIRECTORY = "/data/local/tmp/incrementaldeployment"
69
Lukacs Berkia3a33d72015-08-19 08:34:55 +000070# Some devices support ABIs other than those reported by getprop. In this case,
71# if the most specific ABI is not available in the .apk, we push the more
72# general ones.
73COMPATIBLE_ABIS = {
74 "armeabi-v7a": ["armeabi"],
75 "arm64-v8a": ["armeabi-v7a", "armeabi"]
76}
77
Alex Humeskya4ecde62015-05-21 17:08:42 +000078
79class AdbError(Exception):
80 """An exception class signaling an error in an adb invocation."""
81
82 def __init__(self, args, returncode, stdout, stderr):
83 self.args = args
84 self.returncode = returncode
85 self.stdout = stdout
86 self.stderr = stderr
87 details = "\n".join([
88 "adb command: %s" % args,
89 "return code: %s" % returncode,
90 "stdout: %s" % stdout,
91 "stderr: %s" % stderr,
92 ])
93 super(AdbError, self).__init__(details)
94
95
96class DeviceNotFoundError(Exception):
97 """Raised when the device could not be found."""
98
99
100class MultipleDevicesError(Exception):
101 """Raised when > 1 device is attached and no device serial was given."""
102
103 @staticmethod
104 def CheckError(s):
105 return re.search("more than one (device and emulator|device|emulator)", s)
106
107
108class DeviceUnauthorizedError(Exception):
109 """Raised when the local machine is not authorized to the device."""
110
111
112class TimestampException(Exception):
113 """Raised when there is a problem with timestamp reading/writing."""
114
115
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000116class OldSdkException(Exception):
117 """Raised when the SDK on the target device is older than the app allows."""
118
119
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200120class EnvvarError(Exception):
121 """Raised when a required environment variable is not set."""
122
123
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200124hostpath = os.path
125targetpath = posixpath
126
127
Alex Humeskya4ecde62015-05-21 17:08:42 +0000128class Adb(object):
129 """A class to handle interaction with adb."""
130
Akira Baruahe8a83af2017-12-13 08:33:11 -0800131 def __init__(self, adb_path, temp_dir, adb_jobs, user_home_dir,
132 extra_adb_args):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000133 self._adb_path = adb_path
134 self._temp_dir = temp_dir
135 self._user_home_dir = user_home_dir
136 self._file_counter = 1
137 self._executor = futures.ThreadPoolExecutor(max_workers=adb_jobs)
Akira Baruahe8a83af2017-12-13 08:33:11 -0800138 self._extra_adb_args = extra_adb_args or []
Alex Humeskya4ecde62015-05-21 17:08:42 +0000139
140 def _Exec(self, adb_args):
141 """Executes the given adb command + args."""
Akira Baruahe8a83af2017-12-13 08:33:11 -0800142 args = [self._adb_path] + self._extra_adb_args + adb_args
Alex Humeskya4ecde62015-05-21 17:08:42 +0000143 # TODO(ahumesky): Because multiple instances of adb are executed in
144 # parallel, these debug logging lines will get interleaved.
145 logging.debug("Executing: %s", " ".join(args))
146
147 # adb sometimes requires the user's home directory to access things in
148 # $HOME/.android (e.g. keys to authorize with the device). To avoid any
149 # potential problems with python picking up things in the user's home
150 # directory, HOME is not set in the environment around python and is instead
151 # passed explicitly as a flag.
152 env = {}
153 if self._user_home_dir:
154 env["HOME"] = self._user_home_dir
155
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200156 # On Windows, adb requires the SystemRoot environment variable.
157 if Adb._IsHostOsWindows():
158 value = os.getenv("SYSTEMROOT")
159 if not value:
160 raise EnvvarError(("The %SYSTEMROOT% environment variable must "
161 "be set or Adb won't work"))
162 env["SYSTEMROOT"] = value
163
Alex Humeskya4ecde62015-05-21 17:08:42 +0000164 adb = subprocess.Popen(
165 args,
166 stdin=subprocess.PIPE,
167 stdout=subprocess.PIPE,
168 stderr=subprocess.PIPE,
169 env=env)
Tony Aiutoeecb3442022-08-30 06:33:24 -0700170 raw_stdout, raw_stderr = adb.communicate()
171 # This hackery is to account for a change in what communicate() returns
172 # in Python 3.7. We just deal with it being either string or bytes.
173 if isinstance(raw_stderr, bytes):
174 stdout = raw_stdout.decode("utf-8").strip()
175 stderr = raw_stderr.decode("utf-8").strip()
176 else:
177 stdout = raw_stdout.strip()
178 stderr = raw_stderr.strip()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000179 logging.debug("adb ret: %s", adb.returncode)
180 logging.debug("adb out: %s", stdout)
181 logging.debug("adb err: %s", stderr)
182
183 # Check these first so that the more specific error gets raised instead of
184 # the more generic AdbError.
185 if "device not found" in stderr:
186 raise DeviceNotFoundError()
187 elif "device unauthorized" in stderr:
188 raise DeviceUnauthorizedError()
189 elif MultipleDevicesError.CheckError(stderr):
190 # The error messages are from adb's transport.c, but something adds
191 # "error: " to the beginning, so take it off so that we don't end up
192 # printing "Error: error: ..."
193 raise MultipleDevicesError(re.sub("^error: ", "", stderr))
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000194 elif "INSTALL_FAILED_OLDER_SDK" in stdout:
195 raise OldSdkException()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000196
197 if adb.returncode != 0:
198 raise AdbError(args, adb.returncode, stdout, stderr)
199
200 return adb.returncode, stdout, stderr, args
201
202 def _ExecParallel(self, adb_args):
203 return self._executor.submit(self._Exec, adb_args)
204
205 def _CreateLocalFile(self):
206 """Returns a path to a temporary local file in the temp directory."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200207 local = hostpath.join(self._temp_dir, "adbfile_%d" % self._file_counter)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000208 self._file_counter += 1
209 return local
210
211 def GetInstallTime(self, package):
212 """Get the installation time of a package."""
213 _, stdout, _, _ = self._Shell("dumpsys package %s" % package)
Tony Aiutoeecb3442022-08-30 06:33:24 -0700214 match = re.search("firstInstallTime=(.*)$", stdout, re.MULTILINE)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000215 if match:
216 return match.group(1)
217 else:
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000218 return None
Alex Humeskya4ecde62015-05-21 17:08:42 +0000219
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000220 def GetAbi(self):
221 """Returns the ABI the device supports."""
222 _, stdout, _, _ = self._Shell("getprop ro.product.cpu.abi")
223 return stdout
224
Alex Humeskya4ecde62015-05-21 17:08:42 +0000225 def Push(self, local, remote):
226 """Invoke 'adb push' in parallel."""
227 return self._ExecParallel(["push", local, remote])
228
229 def PushString(self, contents, remote):
230 """Push a given string to a given path on the device in parallel."""
231 local = self._CreateLocalFile()
Laszlo Csomord456fca2017-08-10 10:21:53 +0200232 with open(local, "wb") as f:
Googler246ee562017-12-19 11:04:21 -0800233 f.write(contents.encode("utf-8"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000234 return self.Push(local, remote)
235
236 def Pull(self, remote):
237 """Invoke 'adb pull'.
238
239 Args:
240 remote: The path to the remote file to pull.
241
242 Returns:
Tony Aiutoeecb3442022-08-30 06:33:24 -0700243 The uninterpreted contents of a file or None if the file didn't exist.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000244 """
245 local = self._CreateLocalFile()
246 try:
247 self._Exec(["pull", remote, local])
Tony Aiutoeecb3442022-08-30 06:33:24 -0700248 # Subtle stuff here. We read the file as a blob of bytes, which is bytes
249 # in python3, but want to return it as a str, so we do a no-op decode.
250 # It is up to the caller to re-decode the content if they are reading
251 # a text file that is really UTF-8.
252 # FWIW: Earlier code decoded the content as if it were UTF-8, which is
253 # arguably wrong. This tool sometimes pulls text files and sometimes
254 # binaries. The caller should specify if they want it decoded or not.
Laszlo Csomord456fca2017-08-10 10:21:53 +0200255 with open(local, "rb") as f:
Tony Aiutoeecb3442022-08-30 06:33:24 -0700256 return "".join([chr(b) for b in f.read()])
Alex Humeskya4ecde62015-05-21 17:08:42 +0000257 except (AdbError, IOError):
258 return None
259
260 def InstallMultiple(self, apk, pkg=None):
261 """Invoke 'adb install-multiple'."""
262
263 pkg_args = ["-p", pkg] if pkg else []
264 ret, stdout, stderr, args = self._Exec(
265 ["install-multiple", "-r"] + pkg_args + [apk])
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000266 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000267 raise AdbError(args, ret, stdout, stderr)
268
269 def Install(self, apk):
270 """Invoke 'adb install'."""
271 ret, stdout, stderr, args = self._Exec(["install", "-r", apk])
272
273 # adb install could fail with a message on stdout like this:
274 #
275 # pkg: /data/local/tmp/Gmail_dev_sharded_incremental.apk
276 # Failure [INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES]
277 #
278 # and yet it will still have a return code of 0. At least for the install
279 # command, it will print "Success" if it succeeded, so check for that in
280 # standard out instead of relying on the return code.
Lukacs Berki60f6cf62015-12-16 08:57:12 +0000281 if "FAILED" in stdout or "FAILED" in stderr:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000282 raise AdbError(args, ret, stdout, stderr)
283
Lukacs Berkifaff7182015-08-20 14:27:42 +0000284 def Uninstall(self, pkg):
285 """Invoke 'adb uninstall'."""
286 self._Exec(["uninstall", pkg])
287 # No error checking. If this fails, we assume that the app was not installed
288 # in the first place.
289
Alex Humeskya4ecde62015-05-21 17:08:42 +0000290 def Delete(self, remote):
291 """Delete the given file (or directory) on the device."""
292 self.DeleteMultiple([remote])
293
294 def DeleteMultiple(self, remote_files):
295 """Delete the given files (or directories) on the device."""
296 files_str = " ".join(remote_files)
297 if files_str:
298 self._Shell("rm -fr %s" % files_str)
299
300 def Mkdir(self, d):
301 """Invokes mkdir with the specified directory on the device."""
302 self._Shell("mkdir -p %s" % d)
303
304 def StopApp(self, package):
305 """Force stops the app with the given package."""
306 self._Shell("am force-stop %s" % package)
307
Googler73d4fc92015-07-30 20:39:46 +0000308 def StopAppAndSaveState(self, package):
309 """Stops the app with the given package, saving state for the next run."""
310 # 'am kill' will only kill processes in the background, so we must make sure
311 # our process is in the background first. We accomplish this by bringing up
312 # the app switcher.
313 self._Shell("input keyevent KEYCODE_APP_SWITCH")
314 self._Shell("am kill %s" % package)
315
Googler2b88f622017-03-16 21:52:14 +0000316 def StartApp(self, package, start_type):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000317 """Starts the app with the given package."""
Googler2b88f622017-03-16 21:52:14 +0000318 if start_type == "debug":
319 self._Shell("am set-debug-app -w --persistent %s" % package)
320 else:
321 self._Shell("am clear-debug-app %s" % package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000322 self._Shell("monkey -p %s -c android.intent.category.LAUNCHER 1" % package)
323
324 def _Shell(self, cmd):
325 """Invoke 'adb shell'."""
326 return self._Exec(["shell", cmd])
327
Laszlo Csomorfe14baf2017-08-07 15:47:18 +0200328 @staticmethod
329 def _IsHostOsWindows():
330 return os.name == "nt"
331
Alex Humeskya4ecde62015-05-21 17:08:42 +0000332
333ManifestEntry = collections.namedtuple(
334 "ManifestEntry", ["input_file", "zippath", "installpath", "sha256"])
335
336
337def ParseManifest(contents):
338 """Parses a dexmanifest file.
339
340 Args:
341 contents: the contents of the manifest file to be parsed.
342
343 Returns:
344 A dict of install path -> ManifestEntry.
345 """
346 result = {}
347
348 for l in contents.split("\n"):
349 entry = ManifestEntry(*(l.strip().split(" ")))
350 result[entry.installpath] = entry
351
352 return result
353
354
355def GetAppPackage(stub_datafile):
356 """Returns the app package specified in a stub data file."""
Tony Aiutoeecb3442022-08-30 06:33:24 -0700357 with open(stub_datafile, "r", encoding="utf-8") as f:
358 return f.readlines()[1].strip()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000359
360
361def UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, full_install):
362 """Uploads dexes to the device so that the state.
363
364 Does the minimum amount of work necessary to make the state of the device
365 consistent with what was built.
366
367 Args:
368 adb: the Adb instance representing the device to install to
369 execroot: the execroot
370 app_dir: the directory things should be installed under on the device
371 temp_dir: a local temporary directory
372 dexmanifest: contents of the dex manifest
373 full_install: whether to do a full install
374
375 Returns:
376 None.
377 """
378
379 # Fetch the manifest on the device
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200380 dex_dir = targetpath.join(app_dir, "dex")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000381 adb.Mkdir(dex_dir)
382
383 old_manifest = None
384
385 if not full_install:
386 logging.info("Fetching dex manifest from device...")
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200387 old_manifest_contents = adb.Pull(targetpath.join(dex_dir, "manifest"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000388 if old_manifest_contents:
389 old_manifest = ParseManifest(old_manifest_contents)
390 else:
391 logging.info("Dex manifest not found on device")
392
393 if old_manifest is None:
394 # If the manifest is not found, maybe a previous installation attempt
395 # was interrupted. Wipe the slate clean. Do this also in case we do a full
396 # installation.
397 old_manifest = {}
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200398 adb.Delete(targetpath.join(dex_dir, "*"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000399
400 new_manifest = ParseManifest(dexmanifest)
401 dexes_to_delete = set(old_manifest) - set(new_manifest)
402
403 # Figure out which dexes to upload: those that are present in the new manifest
404 # but not in the old one and those whose checksum was changed
405 common_dexes = set(new_manifest).intersection(old_manifest)
406 dexes_to_upload = set(d for d in common_dexes
407 if new_manifest[d].sha256 != old_manifest[d].sha256)
408 dexes_to_upload.update(set(new_manifest) - set(old_manifest))
409
410 if not dexes_to_delete and not dexes_to_upload:
411 # If we have nothing to do, don't bother removing and rewriting the manifest
412 logging.info("Application dexes up-to-date")
413 return
414
415 # Delete the manifest so that we know how to get back to a consistent state
416 # if we are interrupted.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200417 adb.Delete(targetpath.join(dex_dir, "manifest"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000418
419 # Tuple of (local, remote) files to push to the device.
420 files_to_push = []
421
422 # Sort dexes to be uploaded by the zip file they are in so that we only need
423 # to open each zip only once.
424 dexzips_in_upload = set(new_manifest[d].input_file for d in dexes_to_upload
425 if new_manifest[d].zippath != "-")
426 for i, dexzip_name in enumerate(dexzips_in_upload):
427 zip_dexes = [
428 d for d in dexes_to_upload if new_manifest[d].input_file == dexzip_name]
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200429 dexzip_tempdir = hostpath.join(temp_dir, "dex", str(i))
430 with zipfile.ZipFile(hostpath.join(execroot, dexzip_name)) as dexzip:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000431 for dex in zip_dexes:
432 zippath = new_manifest[dex].zippath
433 dexzip.extract(zippath, dexzip_tempdir)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200434 files_to_push.append((hostpath.join(dexzip_tempdir, zippath),
435 targetpath.join(dex_dir, dex)))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000436
437 # Now gather all the dexes that are not within a .zip file.
438 dexes_to_upload = set(
439 d for d in dexes_to_upload if new_manifest[d].zippath == "-")
440 for dex in dexes_to_upload:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200441 files_to_push.append((new_manifest[dex].input_file, targetpath.join(
442 dex_dir, dex)))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000443
444 num_files = len(dexes_to_delete) + len(files_to_push)
445 logging.info("Updating %d dex%s...", num_files, "es" if num_files > 1 else "")
446
447 # Delete the dexes that are not in the new manifest
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200448 adb.DeleteMultiple(targetpath.join(dex_dir, dex) for dex in dexes_to_delete)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000449
450 # Upload all the files.
451 upload_walltime_start = time.time()
452 fs = [adb.Push(local, remote) for local, remote in files_to_push]
453 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
454 upload_walltime = time.time() - upload_walltime_start
455 logging.debug("Dex upload walltime: %s seconds", upload_walltime)
456
457 # If there is anything in not_done, then some adb call failed and we
458 # can cancel the rest.
459 if not_done:
460 for f in not_done:
461 f.cancel()
462
463 # If any adb call resulted in an exception, re-raise it.
464 for f in done:
465 f.result()
466
467 # If no dex upload failed, upload the manifest. If any upload failed, the
468 # exception should have been re-raised above.
469 # Call result() to raise the exception if there was one.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200470 adb.PushString(dexmanifest, targetpath.join(dex_dir, "manifest")).result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000471
472
473def Checksum(filename):
474 """Compute the SHA-256 checksum of a file."""
475 h = hashlib.sha256()
Laszlo Csomord456fca2017-08-10 10:21:53 +0200476 with open(filename, "rb") as f:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000477 while True:
478 data = f.read(65536)
479 if not data:
480 break
481
482 h.update(data)
483
484 return h.hexdigest()
485
486
487def UploadResources(adb, resource_apk, app_dir):
488 """Uploads resources to the device.
489
490 Args:
491 adb: The Adb instance representing the device to install to.
492 resource_apk: Path to the resource apk.
493 app_dir: The directory things should be installed under on the device.
494
495 Returns:
496 None.
497 """
498
499 # Compute the checksum of the new resources file
500 new_checksum = Checksum(resource_apk)
501
502 # Fetch the checksum of the resources file on the device, if it exists
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200503 device_checksum_file = targetpath.join(app_dir, "resources_checksum")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000504 old_checksum = adb.Pull(device_checksum_file)
505 if old_checksum == new_checksum:
506 logging.info("Application resources up-to-date")
507 return
508 logging.info("Updating application resources...")
509
510 # Remove the checksum file on the device so that if the transfer is
511 # interrupted, we know how to get the device back to a consistent state.
512 adb.Delete(device_checksum_file)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200513 adb.Push(resource_apk, targetpath.join(app_dir, "resources.ap_")).result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000514
515 # Write the new checksum to the device.
516 adb.PushString(new_checksum, device_checksum_file).result()
517
518
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000519def ConvertNativeLibs(args):
520 """Converts the --native_libs command line argument to an arch -> libs map."""
521 native_libs = {}
522 if args is not None:
523 for native_lib in args:
Tony Aiutoeecb3442022-08-30 06:33:24 -0700524 abi, path = native_lib.split(":")
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000525 if abi not in native_libs:
526 native_libs[abi] = set()
527
528 native_libs[abi].add(path)
529
530 return native_libs
531
532
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000533def FindAbi(device_abi, app_abis):
534 """Selects which ABI native libs should be installed for."""
535 if device_abi in app_abis:
536 return device_abi
537
538 if device_abi in COMPATIBLE_ABIS:
539 for abi in COMPATIBLE_ABIS[device_abi]:
540 if abi in app_abis:
541 logging.warn("App does not have native libs for ABI '%s'. Using ABI "
542 "'%s'.", device_abi, abi)
543 return abi
544
545 logging.warn("No native libs for device ABI '%s'. App has native libs for "
546 "ABIs: %s", device_abi, ", ".join(app_abis))
547 return None
548
549
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000550def UploadNativeLibs(adb, native_lib_args, app_dir, full_install):
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000551 """Uploads native libraries to the device."""
552
553 native_libs = ConvertNativeLibs(native_lib_args)
554 libs = set()
555 if native_libs:
laszlocsomord93a1462019-11-04 09:14:39 -0800556 abi = FindAbi(adb.GetAbi(), list(native_libs.keys()))
Lukacs Berkia3a33d72015-08-19 08:34:55 +0000557 if abi:
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000558 libs = native_libs[abi]
559
560 basename_to_path = {}
561 install_checksums = {}
562 for lib in sorted(libs):
563 install_checksums[os.path.basename(lib)] = Checksum(lib)
564 basename_to_path[os.path.basename(lib)] = lib
565
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000566 device_manifest = None
567 if not full_install:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200568 device_manifest = adb.Pull(
569 targetpath.join(app_dir, "native", "native_manifest"))
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000570
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000571 device_checksums = {}
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000572 if device_manifest is None:
573 # If we couldn't fetch the device manifest or if this is a non-incremental
574 # install, wipe the slate clean
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200575 adb.Delete(targetpath.join(app_dir, "native"))
jingwenb3f28d32018-12-10 19:30:57 -0800576
577 # From Android 28 onwards, `adb push` creates directories with insufficient
578 # permissions, resulting in errors when pushing files. `adb shell mkdir`
579 # works correctly however, so we create the directory here.
580 # See https://github.com/bazelbuild/examples/issues/77 for more information.
581 adb.Mkdir(targetpath.join(app_dir, "native"))
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000582 else:
583 # Otherwise, parse the manifest. Note that this branch is also taken if the
584 # manifest is empty.
Lukacs Berkiff6ef9b2015-08-14 08:17:41 +0000585 for manifest_line in device_manifest.split("\n"):
586 if manifest_line:
587 name, checksum = manifest_line.split(" ")
588 device_checksums[name] = checksum
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000589
590 libs_to_delete = set(device_checksums) - set(install_checksums)
591 libs_to_upload = set(install_checksums) - set(device_checksums)
592 common_libs = set(install_checksums).intersection(set(device_checksums))
593 libs_to_upload.update([l for l in common_libs
594 if install_checksums[l] != device_checksums[l]])
595
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200596 libs_to_push = [(basename_to_path[lib], targetpath.join(
597 app_dir, "native", lib)) for lib in libs_to_upload]
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000598
599 if not libs_to_delete and not libs_to_push and device_manifest is not None:
600 logging.info("Native libs up-to-date")
601 return
602
603 num_files = len(libs_to_delete) + len(libs_to_push)
604 logging.info("Updating %d native lib%s...",
605 num_files, "s" if num_files != 1 else "")
606
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200607 adb.Delete(targetpath.join(app_dir, "native", "native_manifest"))
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000608
609 if libs_to_delete:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200610 adb.DeleteMultiple(
611 [targetpath.join(app_dir, "native", lib) for lib in libs_to_delete])
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000612
613 upload_walltime_start = time.time()
614 fs = [adb.Push(local, remote) for local, remote in libs_to_push]
615 done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
616 upload_walltime = time.time() - upload_walltime_start
617 logging.debug("Native library upload walltime: %s seconds", upload_walltime)
618
619 # If there is anything in not_done, then some adb call failed and we
620 # can cancel the rest.
621 if not_done:
622 for f in not_done:
623 f.cancel()
624
625 # If any adb call resulted in an exception, re-raise it.
626 for f in done:
627 f.result()
628
629 install_manifest = [
Tony Aiutoeecb3442022-08-30 06:33:24 -0700630 name + " " + checksum
laszlocsomord93a1462019-11-04 09:14:39 -0800631 for name, checksum in install_checksums.items()
632 ]
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000633 adb.PushString("\n".join(install_manifest),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200634 targetpath.join(app_dir, "native",
635 "native_manifest")).result()
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000636
637
Alex Humeskya4ecde62015-05-21 17:08:42 +0000638def VerifyInstallTimestamp(adb, app_package):
639 """Verifies that the app is unchanged since the last mobile-install."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200640 expected_timestamp = adb.Pull(
641 targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000642 if not expected_timestamp:
643 raise TimestampException(
644 "Cannot verify last mobile install. At least one non-incremental "
645 "'mobile-install' must precede incremental installs")
646
647 actual_timestamp = adb.GetInstallTime(app_package)
Lukacs Berki0dffeac2015-09-15 07:18:47 +0000648 if actual_timestamp is None:
649 raise TimestampException(
650 "Package '%s' is not installed on the device. At least one "
651 "non-incremental 'mobile-install' must precede incremental "
652 "installs." % app_package)
653
Alex Humeskya4ecde62015-05-21 17:08:42 +0000654 if actual_timestamp != expected_timestamp:
655 raise TimestampException("Installed app '%s' has an unexpected timestamp. "
656 "Did you last install the app in a way other than "
657 "'mobile-install'?" % app_package)
658
659
Lukacs Berkifaff7182015-08-20 14:27:42 +0000660def SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
661 split_apks):
662 """Does incremental installation using split packages."""
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200663 app_dir = targetpath.join(DEVICE_DIRECTORY, app_package)
664 device_manifest_path = targetpath.join(app_dir, "split_manifest")
Lukacs Berkifaff7182015-08-20 14:27:42 +0000665 device_manifest = adb.Pull(device_manifest_path)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200666 expected_timestamp = adb.Pull(targetpath.join(app_dir, "install_timestamp"))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000667 actual_timestamp = adb.GetInstallTime(app_package)
668 device_checksums = {}
669 if device_manifest is not None:
670 for manifest_line in device_manifest.split("\n"):
671 if manifest_line:
672 name, checksum = manifest_line.split(" ")
673 device_checksums[name] = checksum
674
675 install_checksums = {}
676 install_checksums["__MAIN__"] = Checksum(
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200677 hostpath.join(execroot, split_main_apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000678 for apk in split_apks:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200679 install_checksums[apk] = Checksum(hostpath.join(execroot, apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000680
681 reinstall_main = False
682 if (device_manifest is None or actual_timestamp is None or
683 actual_timestamp != expected_timestamp or
684 install_checksums["__MAIN__"] != device_checksums["__MAIN__"] or
685 set(device_checksums.keys()) != set(install_checksums.keys())):
686 # The main app is not up to date or not present or something happened
687 # with the on-device manifest. Start from scratch. Notably, we cannot
688 # uninstall a split package, so if the set of packages changes, we also
689 # need to do a full reinstall.
690 reinstall_main = True
691 device_checksums = {}
692
693 apks_to_update = [
694 apk for apk in split_apks if
695 apk not in device_checksums or
696 device_checksums[apk] != install_checksums[apk]]
697
698 if not apks_to_update and not reinstall_main:
699 # Nothing to do
700 return
701
702 # Delete the device manifest so that if something goes wrong, we do a full
703 # reinstall next time
704 adb.Delete(device_manifest_path)
705
706 if reinstall_main:
707 logging.info("Installing main APK...")
708 adb.Uninstall(app_package)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200709 adb.InstallMultiple(targetpath.join(execroot, split_main_apk))
Lukacs Berkifaff7182015-08-20 14:27:42 +0000710 adb.PushString(
711 adb.GetInstallTime(app_package),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200712 targetpath.join(app_dir, "install_timestamp")).result()
Lukacs Berkifaff7182015-08-20 14:27:42 +0000713
714 logging.info("Reinstalling %s APKs...", len(apks_to_update))
715
716 for apk in apks_to_update:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200717 adb.InstallMultiple(targetpath.join(execroot, apk), app_package)
Lukacs Berkifaff7182015-08-20 14:27:42 +0000718
719 install_manifest = [
Tony Aiutoeecb3442022-08-30 06:33:24 -0700720 name + " " + checksum
laszlocsomord93a1462019-11-04 09:14:39 -0800721 for name, checksum in install_checksums.items()
722 ]
Lukacs Berkifaff7182015-08-20 14:27:42 +0000723 adb.PushString("\n".join(install_manifest),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200724 targetpath.join(app_dir, "split_manifest")).result()
Lukacs Berkifaff7182015-08-20 14:27:42 +0000725
726
Akira Baruahe8a83af2017-12-13 08:33:11 -0800727def IncrementalInstall(adb_path,
728 execroot,
729 stub_datafile,
730 output_marker,
731 adb_jobs,
732 start_type,
733 dexmanifest=None,
734 apk=None,
735 native_libs=None,
736 resource_apk=None,
737 split_main_apk=None,
738 split_apks=None,
739 user_home_dir=None,
740 extra_adb_args=None):
Alex Humeskya4ecde62015-05-21 17:08:42 +0000741 """Performs an incremental install.
742
743 Args:
744 adb_path: Path to the adb executable.
745 execroot: Exec root.
746 stub_datafile: The stub datafile containing the app's package name.
747 output_marker: Path to the output marker file.
748 adb_jobs: The number of instances of adb to use in parallel.
Googler73d4fc92015-07-30 20:39:46 +0000749 start_type: A string describing whether/how to start the app after
750 installing it. Can be 'no', 'cold', or 'warm'.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000751 dexmanifest: Path to the .dex manifest file.
752 apk: Path to the .apk file. May be None to perform an incremental install.
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000753 native_libs: Native libraries to install.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000754 resource_apk: Path to the apk containing the app's resources.
755 split_main_apk: the split main .apk if split installation is desired.
756 split_apks: the list of split .apks to be installed.
757 user_home_dir: Path to the user's home directory.
Akira Baruahe8a83af2017-12-13 08:33:11 -0800758 extra_adb_args: Extra arguments that will always be passed to adb.
Alex Humeskya4ecde62015-05-21 17:08:42 +0000759 """
760 temp_dir = tempfile.mkdtemp()
761 try:
Akira Baruahe8a83af2017-12-13 08:33:11 -0800762 adb = Adb(adb_path, temp_dir, adb_jobs, user_home_dir, extra_adb_args)
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200763 app_package = GetAppPackage(hostpath.join(execroot, stub_datafile))
764 app_dir = targetpath.join(DEVICE_DIRECTORY, app_package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000765 if split_main_apk:
Lukacs Berkifaff7182015-08-20 14:27:42 +0000766 SplitIncrementalInstall(adb, app_package, execroot, split_main_apk,
767 split_apks)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000768 else:
769 if not apk:
770 VerifyInstallTimestamp(adb, app_package)
771
Tony Aiutoeecb3442022-08-30 06:33:24 -0700772 with open(hostpath.join(execroot, dexmanifest), "r",
773 encoding="utf-8") as f:
774 dexmanifest_content = f.read()
775 UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest_content,
776 bool(apk))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000777 # TODO(ahumesky): UploadDexes waits for all the dexes to be uploaded, and
778 # then UploadResources is called. We could instead enqueue everything
779 # onto the threadpool so that uploading resources happens sooner.
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200780 UploadResources(adb, hostpath.join(execroot, resource_apk), app_dir)
Lukacs Berkib4b19bc2015-07-30 11:45:35 +0000781 UploadNativeLibs(adb, native_libs, app_dir, bool(apk))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000782 if apk:
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200783 apk_path = targetpath.join(execroot, apk)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000784 adb.Install(apk_path)
785 future = adb.PushString(
786 adb.GetInstallTime(app_package),
Laszlo Csomord4e673e2017-06-29 18:02:52 +0200787 targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp"))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000788 future.result()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000789 else:
Googler73d4fc92015-07-30 20:39:46 +0000790 if start_type == "warm":
791 adb.StopAppAndSaveState(app_package)
792 else:
793 adb.StopApp(app_package)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000794
Googler2b88f622017-03-16 21:52:14 +0000795 if start_type in ["cold", "warm", "debug"]:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000796 logging.info("Starting application %s", app_package)
Googler2b88f622017-03-16 21:52:14 +0000797 adb.StartApp(app_package, start_type)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000798
Laszlo Csomord456fca2017-08-10 10:21:53 +0200799 with open(output_marker, "wb") as _:
Alex Humeskya4ecde62015-05-21 17:08:42 +0000800 pass
801 except DeviceNotFoundError:
802 sys.exit("Error: Device not found")
803 except DeviceUnauthorizedError:
804 sys.exit("Error: Device unauthorized. Please check the confirmation "
805 "dialog on your device.")
806 except MultipleDevicesError as e:
Googler246ee562017-12-19 11:04:21 -0800807 sys.exit("Error: " + str(e) + "\nTry specifying a device serial with "
Dan Fabulich1d35ca02018-07-05 16:08:06 -0700808 "\"bazel mobile-install --adb_arg=-s --adb_arg=$ANDROID_SERIAL\"")
Alex Humeskyf0a5ac62015-09-22 00:41:11 +0000809 except OldSdkException as e:
810 sys.exit("Error: The device does not support the API level specified in "
811 "the application's manifest. Check minSdkVersion in "
812 "AndroidManifest.xml")
Alex Humeskya4ecde62015-05-21 17:08:42 +0000813 except TimestampException as e:
Googler246ee562017-12-19 11:04:21 -0800814 sys.exit("Error:\n%s" % str(e))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000815 except AdbError as e:
Googler246ee562017-12-19 11:04:21 -0800816 sys.exit("Error:\n%s" % str(e))
Alex Humeskya4ecde62015-05-21 17:08:42 +0000817 finally:
818 shutil.rmtree(temp_dir, True)
819
820
laszlocsomord93a1462019-11-04 09:14:39 -0800821def main(unused_argv):
822 if FLAGS.verbosity == "1": # 'verbosity' flag is defined in absl.logging
Alex Humeskya4ecde62015-05-21 17:08:42 +0000823 level = logging.DEBUG
824 fmt = "%(levelname)-5s %(asctime)s %(module)s:%(lineno)3d] %(message)s"
825 else:
826 level = logging.INFO
827 fmt = "%(message)s"
828 logging.basicConfig(stream=sys.stdout, level=level, format=fmt)
829
Googler73d4fc92015-07-30 20:39:46 +0000830 start_type = FLAGS.start
831 if FLAGS.start_app and start_type == "no":
832 start_type = "cold"
833
Alex Humeskya4ecde62015-05-21 17:08:42 +0000834 IncrementalInstall(
835 adb_path=FLAGS.adb,
836 adb_jobs=FLAGS.adb_jobs,
837 execroot=FLAGS.execroot,
838 stub_datafile=FLAGS.stub_datafile,
839 output_marker=FLAGS.output_marker,
Googler73d4fc92015-07-30 20:39:46 +0000840 start_type=start_type,
Lukacs Berkiecbf2422015-07-28 07:57:45 +0000841 native_libs=FLAGS.native_lib,
Alex Humeskya4ecde62015-05-21 17:08:42 +0000842 split_main_apk=FLAGS.split_main_apk,
843 split_apks=FLAGS.split_apk,
844 dexmanifest=FLAGS.dexmanifest,
845 apk=FLAGS.apk,
846 resource_apk=FLAGS.resource_apk,
Akira Baruahe8a83af2017-12-13 08:33:11 -0800847 user_home_dir=FLAGS.user_home_dir,
848 extra_adb_args=FLAGS.extra_adb_arg)
Alex Humeskya4ecde62015-05-21 17:08:42 +0000849
850
851if __name__ == "__main__":
852 FLAGS(sys.argv)
853 # process any additional flags in --flagfile
854 if FLAGS.flagfile:
Laszlo Csomord456fca2017-08-10 10:21:53 +0200855 with open(FLAGS.flagfile, "rb") as flagsfile:
Lukacs Berki682ad922015-06-11 08:01:07 +0000856 FLAGS.Reset()
Alex Humeskya4ecde62015-05-21 17:08:42 +0000857 FLAGS(sys.argv + [line.strip() for line in flagsfile.readlines()])
Lukacs Berki682ad922015-06-11 08:01:07 +0000858
laszlocsomord93a1462019-11-04 09:14:39 -0800859 app.run(main)