| # Lint as: python2, python3 |
| # pylint: disable=g-direct-third-party-import |
| # pylint: disable=g-bad-file-header |
| # Copyright 2015 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Installs an Android application, possibly in an incremental way.""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import collections |
| from concurrent import futures |
| import hashlib |
| import logging |
| import os |
| import posixpath |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| import zipfile |
| |
| # Do not edit this line. Copybara replaces it with PY2 migration helper. |
| from absl import app |
| from absl import flags |
| import six |
| |
| flags.DEFINE_string("split_main_apk", None, "The main APK for split install") |
| flags.DEFINE_multi_string("split_apk", [], "Split APKs to install") |
| flags.DEFINE_string("dexmanifest", None, "The .dex manifest") |
| flags.DEFINE_multi_string("native_lib", None, "Native libraries to install") |
| flags.DEFINE_string("resource_apk", None, "The resource .apk") |
| flags.DEFINE_string( |
| "apk", None, "The app .apk. If not specified, " |
| "do incremental deployment") |
| flags.DEFINE_string("adb", None, "ADB to use") |
| flags.DEFINE_string("stub_datafile", None, "The stub data file") |
| flags.DEFINE_string("output_marker", None, "The output marker file") |
| flags.DEFINE_multi_string("extra_adb_arg", [], "Extra arguments to adb") |
| flags.DEFINE_string("execroot", ".", "The exec root") |
| flags.DEFINE_integer( |
| "adb_jobs", |
| 2, "The number of instances of adb to use in parallel to " |
| "update files on the device", |
| lower_bound=1) |
| flags.DEFINE_enum( |
| "start", "no", ["no", "cold", "warm", "debug"], |
| "Whether/how to start the app after installing it. 'cold' " |
| "and 'warm' will both cause the app to be started, 'warm' " |
| "will start it with previously saved application state, " |
| "'debug' will wait for the debugger before a clean start.") |
| flags.DEFINE_boolean("start_app", False, "Deprecated, use 'start'.") |
| flags.DEFINE_string("user_home_dir", None, "Path to the user's home directory") |
| flags.DEFINE_string("flagfile", None, |
| "Path to a file to read additional flags from") |
| |
| FLAGS = flags.FLAGS |
| |
| DEVICE_DIRECTORY = "/data/local/tmp/incrementaldeployment" |
| |
| # Some devices support ABIs other than those reported by getprop. In this case, |
| # if the most specific ABI is not available in the .apk, we push the more |
| # general ones. |
| COMPATIBLE_ABIS = { |
| "armeabi-v7a": ["armeabi"], |
| "arm64-v8a": ["armeabi-v7a", "armeabi"] |
| } |
| |
| |
| class AdbError(Exception): |
| """An exception class signaling an error in an adb invocation.""" |
| |
| def __init__(self, args, returncode, stdout, stderr): |
| self.args = args |
| self.returncode = returncode |
| self.stdout = stdout |
| self.stderr = stderr |
| details = "\n".join([ |
| "adb command: %s" % args, |
| "return code: %s" % returncode, |
| "stdout: %s" % stdout, |
| "stderr: %s" % stderr, |
| ]) |
| super(AdbError, self).__init__(details) |
| |
| |
| class DeviceNotFoundError(Exception): |
| """Raised when the device could not be found.""" |
| |
| |
| class MultipleDevicesError(Exception): |
| """Raised when > 1 device is attached and no device serial was given.""" |
| |
| @staticmethod |
| def CheckError(s): |
| return re.search("more than one (device and emulator|device|emulator)", s) |
| |
| |
| class DeviceUnauthorizedError(Exception): |
| """Raised when the local machine is not authorized to the device.""" |
| |
| |
| class TimestampException(Exception): |
| """Raised when there is a problem with timestamp reading/writing.""" |
| |
| |
| class OldSdkException(Exception): |
| """Raised when the SDK on the target device is older than the app allows.""" |
| |
| |
| class EnvvarError(Exception): |
| """Raised when a required environment variable is not set.""" |
| |
| |
| hostpath = os.path |
| targetpath = posixpath |
| |
| |
| class Adb(object): |
| """A class to handle interaction with adb.""" |
| |
| def __init__(self, adb_path, temp_dir, adb_jobs, user_home_dir, |
| extra_adb_args): |
| self._adb_path = adb_path |
| self._temp_dir = temp_dir |
| self._user_home_dir = user_home_dir |
| self._file_counter = 1 |
| self._executor = futures.ThreadPoolExecutor(max_workers=adb_jobs) |
| self._extra_adb_args = extra_adb_args or [] |
| |
| def _Exec(self, adb_args): |
| """Executes the given adb command + args.""" |
| args = [self._adb_path] + self._extra_adb_args + adb_args |
| # TODO(ahumesky): Because multiple instances of adb are executed in |
| # parallel, these debug logging lines will get interleaved. |
| logging.debug("Executing: %s", " ".join(args)) |
| |
| # adb sometimes requires the user's home directory to access things in |
| # $HOME/.android (e.g. keys to authorize with the device). To avoid any |
| # potential problems with python picking up things in the user's home |
| # directory, HOME is not set in the environment around python and is instead |
| # passed explicitly as a flag. |
| env = {} |
| if self._user_home_dir: |
| env["HOME"] = self._user_home_dir |
| |
| # On Windows, adb requires the SystemRoot environment variable. |
| if Adb._IsHostOsWindows(): |
| value = os.getenv("SYSTEMROOT") |
| if not value: |
| raise EnvvarError(("The %SYSTEMROOT% environment variable must " |
| "be set or Adb won't work")) |
| env["SYSTEMROOT"] = value |
| |
| adb = subprocess.Popen( |
| args, |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| env=env) |
| stdout, stderr = adb.communicate() |
| stdout = stdout.strip() |
| stderr = stderr.strip() |
| logging.debug("adb ret: %s", adb.returncode) |
| logging.debug("adb out: %s", stdout) |
| logging.debug("adb err: %s", stderr) |
| |
| # Check these first so that the more specific error gets raised instead of |
| # the more generic AdbError. |
| stdout = six.ensure_str(stdout) |
| stderr = six.ensure_str(stderr) |
| if "device not found" in stderr: |
| raise DeviceNotFoundError() |
| elif "device unauthorized" in stderr: |
| raise DeviceUnauthorizedError() |
| elif MultipleDevicesError.CheckError(stderr): |
| # The error messages are from adb's transport.c, but something adds |
| # "error: " to the beginning, so take it off so that we don't end up |
| # printing "Error: error: ..." |
| raise MultipleDevicesError(re.sub("^error: ", "", stderr)) |
| elif "INSTALL_FAILED_OLDER_SDK" in stdout: |
| raise OldSdkException() |
| |
| if adb.returncode != 0: |
| raise AdbError(args, adb.returncode, stdout, stderr) |
| |
| return adb.returncode, stdout, stderr, args |
| |
| def _ExecParallel(self, adb_args): |
| return self._executor.submit(self._Exec, adb_args) |
| |
| def _CreateLocalFile(self): |
| """Returns a path to a temporary local file in the temp directory.""" |
| local = hostpath.join(self._temp_dir, "adbfile_%d" % self._file_counter) |
| self._file_counter += 1 |
| return local |
| |
| def GetInstallTime(self, package): |
| """Get the installation time of a package.""" |
| _, stdout, _, _ = self._Shell("dumpsys package %s" % package) |
| match = re.search("firstInstallTime=(.*)$", six.ensure_str(stdout), |
| re.MULTILINE) |
| if match: |
| return match.group(1) |
| else: |
| return None |
| |
| def GetAbi(self): |
| """Returns the ABI the device supports.""" |
| _, stdout, _, _ = self._Shell("getprop ro.product.cpu.abi") |
| return stdout |
| |
| def Push(self, local, remote): |
| """Invoke 'adb push' in parallel.""" |
| return self._ExecParallel(["push", local, remote]) |
| |
| def PushString(self, contents, remote): |
| """Push a given string to a given path on the device in parallel.""" |
| local = self._CreateLocalFile() |
| with open(local, "wb") as f: |
| f.write(contents.encode("utf-8")) |
| return self.Push(local, remote) |
| |
| def Pull(self, remote): |
| """Invoke 'adb pull'. |
| |
| Args: |
| remote: The path to the remote file to pull. |
| |
| Returns: |
| The contents of a file or None if the file didn't exist. |
| """ |
| local = self._CreateLocalFile() |
| try: |
| self._Exec(["pull", remote, local]) |
| with open(local, "rb") as f: |
| return six.ensure_str(f.read(), "utf-8") |
| except (AdbError, IOError): |
| return None |
| |
| def InstallMultiple(self, apk, pkg=None): |
| """Invoke 'adb install-multiple'.""" |
| |
| pkg_args = ["-p", pkg] if pkg else [] |
| ret, stdout, stderr, args = self._Exec( |
| ["install-multiple", "-r"] + pkg_args + [apk]) |
| if "FAILED" in stdout or "FAILED" in stderr: |
| raise AdbError(args, ret, stdout, stderr) |
| |
| def Install(self, apk): |
| """Invoke 'adb install'.""" |
| ret, stdout, stderr, args = self._Exec(["install", "-r", apk]) |
| |
| # adb install could fail with a message on stdout like this: |
| # |
| # pkg: /data/local/tmp/Gmail_dev_sharded_incremental.apk |
| # Failure [INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES] |
| # |
| # and yet it will still have a return code of 0. At least for the install |
| # command, it will print "Success" if it succeeded, so check for that in |
| # standard out instead of relying on the return code. |
| if "FAILED" in stdout or "FAILED" in stderr: |
| raise AdbError(args, ret, stdout, stderr) |
| |
| def Uninstall(self, pkg): |
| """Invoke 'adb uninstall'.""" |
| self._Exec(["uninstall", pkg]) |
| # No error checking. If this fails, we assume that the app was not installed |
| # in the first place. |
| |
| def Delete(self, remote): |
| """Delete the given file (or directory) on the device.""" |
| self.DeleteMultiple([remote]) |
| |
| def DeleteMultiple(self, remote_files): |
| """Delete the given files (or directories) on the device.""" |
| files_str = " ".join(remote_files) |
| if files_str: |
| self._Shell("rm -fr %s" % files_str) |
| |
| def Mkdir(self, d): |
| """Invokes mkdir with the specified directory on the device.""" |
| self._Shell("mkdir -p %s" % d) |
| |
| def StopApp(self, package): |
| """Force stops the app with the given package.""" |
| self._Shell("am force-stop %s" % package) |
| |
| def StopAppAndSaveState(self, package): |
| """Stops the app with the given package, saving state for the next run.""" |
| # 'am kill' will only kill processes in the background, so we must make sure |
| # our process is in the background first. We accomplish this by bringing up |
| # the app switcher. |
| self._Shell("input keyevent KEYCODE_APP_SWITCH") |
| self._Shell("am kill %s" % package) |
| |
| def StartApp(self, package, start_type): |
| """Starts the app with the given package.""" |
| if start_type == "debug": |
| self._Shell("am set-debug-app -w --persistent %s" % package) |
| else: |
| self._Shell("am clear-debug-app %s" % package) |
| self._Shell("monkey -p %s -c android.intent.category.LAUNCHER 1" % package) |
| |
| def _Shell(self, cmd): |
| """Invoke 'adb shell'.""" |
| return self._Exec(["shell", cmd]) |
| |
| @staticmethod |
| def _IsHostOsWindows(): |
| return os.name == "nt" |
| |
| |
| ManifestEntry = collections.namedtuple( |
| "ManifestEntry", ["input_file", "zippath", "installpath", "sha256"]) |
| |
| |
| def ParseManifest(contents): |
| """Parses a dexmanifest file. |
| |
| Args: |
| contents: the contents of the manifest file to be parsed. |
| |
| Returns: |
| A dict of install path -> ManifestEntry. |
| """ |
| result = {} |
| |
| for l in contents.split("\n"): |
| entry = ManifestEntry(*(l.strip().split(" "))) |
| result[entry.installpath] = entry |
| |
| return result |
| |
| |
| def GetAppPackage(stub_datafile): |
| """Returns the app package specified in a stub data file.""" |
| with open(stub_datafile, "rb") as f: |
| return six.ensure_str(f.readlines()[1], "utf-8").strip() |
| |
| |
| def UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, full_install): |
| """Uploads dexes to the device so that the state. |
| |
| Does the minimum amount of work necessary to make the state of the device |
| consistent with what was built. |
| |
| Args: |
| adb: the Adb instance representing the device to install to |
| execroot: the execroot |
| app_dir: the directory things should be installed under on the device |
| temp_dir: a local temporary directory |
| dexmanifest: contents of the dex manifest |
| full_install: whether to do a full install |
| |
| Returns: |
| None. |
| """ |
| |
| # Fetch the manifest on the device |
| dex_dir = targetpath.join(app_dir, "dex") |
| adb.Mkdir(dex_dir) |
| |
| old_manifest = None |
| |
| if not full_install: |
| logging.info("Fetching dex manifest from device...") |
| old_manifest_contents = adb.Pull(targetpath.join(dex_dir, "manifest")) |
| if old_manifest_contents: |
| old_manifest = ParseManifest(old_manifest_contents) |
| else: |
| logging.info("Dex manifest not found on device") |
| |
| if old_manifest is None: |
| # If the manifest is not found, maybe a previous installation attempt |
| # was interrupted. Wipe the slate clean. Do this also in case we do a full |
| # installation. |
| old_manifest = {} |
| adb.Delete(targetpath.join(dex_dir, "*")) |
| |
| new_manifest = ParseManifest(dexmanifest) |
| dexes_to_delete = set(old_manifest) - set(new_manifest) |
| |
| # Figure out which dexes to upload: those that are present in the new manifest |
| # but not in the old one and those whose checksum was changed |
| common_dexes = set(new_manifest).intersection(old_manifest) |
| dexes_to_upload = set(d for d in common_dexes |
| if new_manifest[d].sha256 != old_manifest[d].sha256) |
| dexes_to_upload.update(set(new_manifest) - set(old_manifest)) |
| |
| if not dexes_to_delete and not dexes_to_upload: |
| # If we have nothing to do, don't bother removing and rewriting the manifest |
| logging.info("Application dexes up-to-date") |
| return |
| |
| # Delete the manifest so that we know how to get back to a consistent state |
| # if we are interrupted. |
| adb.Delete(targetpath.join(dex_dir, "manifest")) |
| |
| # Tuple of (local, remote) files to push to the device. |
| files_to_push = [] |
| |
| # Sort dexes to be uploaded by the zip file they are in so that we only need |
| # to open each zip only once. |
| dexzips_in_upload = set(new_manifest[d].input_file for d in dexes_to_upload |
| if new_manifest[d].zippath != "-") |
| for i, dexzip_name in enumerate(dexzips_in_upload): |
| zip_dexes = [ |
| d for d in dexes_to_upload if new_manifest[d].input_file == dexzip_name] |
| dexzip_tempdir = hostpath.join(temp_dir, "dex", str(i)) |
| with zipfile.ZipFile(hostpath.join(execroot, dexzip_name)) as dexzip: |
| for dex in zip_dexes: |
| zippath = new_manifest[dex].zippath |
| dexzip.extract(zippath, dexzip_tempdir) |
| files_to_push.append((hostpath.join(dexzip_tempdir, zippath), |
| targetpath.join(dex_dir, dex))) |
| |
| # Now gather all the dexes that are not within a .zip file. |
| dexes_to_upload = set( |
| d for d in dexes_to_upload if new_manifest[d].zippath == "-") |
| for dex in dexes_to_upload: |
| files_to_push.append((new_manifest[dex].input_file, targetpath.join( |
| dex_dir, dex))) |
| |
| num_files = len(dexes_to_delete) + len(files_to_push) |
| logging.info("Updating %d dex%s...", num_files, "es" if num_files > 1 else "") |
| |
| # Delete the dexes that are not in the new manifest |
| adb.DeleteMultiple(targetpath.join(dex_dir, dex) for dex in dexes_to_delete) |
| |
| # Upload all the files. |
| upload_walltime_start = time.time() |
| fs = [adb.Push(local, remote) for local, remote in files_to_push] |
| done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION) |
| upload_walltime = time.time() - upload_walltime_start |
| logging.debug("Dex upload walltime: %s seconds", upload_walltime) |
| |
| # If there is anything in not_done, then some adb call failed and we |
| # can cancel the rest. |
| if not_done: |
| for f in not_done: |
| f.cancel() |
| |
| # If any adb call resulted in an exception, re-raise it. |
| for f in done: |
| f.result() |
| |
| # If no dex upload failed, upload the manifest. If any upload failed, the |
| # exception should have been re-raised above. |
| # Call result() to raise the exception if there was one. |
| adb.PushString(dexmanifest, targetpath.join(dex_dir, "manifest")).result() |
| |
| |
| def Checksum(filename): |
| """Compute the SHA-256 checksum of a file.""" |
| h = hashlib.sha256() |
| with open(filename, "rb") as f: |
| while True: |
| data = f.read(65536) |
| if not data: |
| break |
| |
| h.update(data) |
| |
| return h.hexdigest() |
| |
| |
| def UploadResources(adb, resource_apk, app_dir): |
| """Uploads resources to the device. |
| |
| Args: |
| adb: The Adb instance representing the device to install to. |
| resource_apk: Path to the resource apk. |
| app_dir: The directory things should be installed under on the device. |
| |
| Returns: |
| None. |
| """ |
| |
| # Compute the checksum of the new resources file |
| new_checksum = Checksum(resource_apk) |
| |
| # Fetch the checksum of the resources file on the device, if it exists |
| device_checksum_file = targetpath.join(app_dir, "resources_checksum") |
| old_checksum = adb.Pull(device_checksum_file) |
| if old_checksum == new_checksum: |
| logging.info("Application resources up-to-date") |
| return |
| logging.info("Updating application resources...") |
| |
| # Remove the checksum file on the device so that if the transfer is |
| # interrupted, we know how to get the device back to a consistent state. |
| adb.Delete(device_checksum_file) |
| adb.Push(resource_apk, targetpath.join(app_dir, "resources.ap_")).result() |
| |
| # Write the new checksum to the device. |
| adb.PushString(new_checksum, device_checksum_file).result() |
| |
| |
| def ConvertNativeLibs(args): |
| """Converts the --native_libs command line argument to an arch -> libs map.""" |
| native_libs = {} |
| if args is not None: |
| for native_lib in args: |
| abi, path = six.ensure_str(native_lib).split(":") |
| if abi not in native_libs: |
| native_libs[abi] = set() |
| |
| native_libs[abi].add(path) |
| |
| return native_libs |
| |
| |
| def FindAbi(device_abi, app_abis): |
| """Selects which ABI native libs should be installed for.""" |
| if device_abi in app_abis: |
| return device_abi |
| |
| if device_abi in COMPATIBLE_ABIS: |
| for abi in COMPATIBLE_ABIS[device_abi]: |
| if abi in app_abis: |
| logging.warn("App does not have native libs for ABI '%s'. Using ABI " |
| "'%s'.", device_abi, abi) |
| return abi |
| |
| logging.warn("No native libs for device ABI '%s'. App has native libs for " |
| "ABIs: %s", device_abi, ", ".join(app_abis)) |
| return None |
| |
| |
| def UploadNativeLibs(adb, native_lib_args, app_dir, full_install): |
| """Uploads native libraries to the device.""" |
| |
| native_libs = ConvertNativeLibs(native_lib_args) |
| libs = set() |
| if native_libs: |
| abi = FindAbi(adb.GetAbi(), list(native_libs.keys())) |
| if abi: |
| libs = native_libs[abi] |
| |
| basename_to_path = {} |
| install_checksums = {} |
| for lib in sorted(libs): |
| install_checksums[os.path.basename(lib)] = Checksum(lib) |
| basename_to_path[os.path.basename(lib)] = lib |
| |
| device_manifest = None |
| if not full_install: |
| device_manifest = adb.Pull( |
| targetpath.join(app_dir, "native", "native_manifest")) |
| |
| device_checksums = {} |
| if device_manifest is None: |
| # If we couldn't fetch the device manifest or if this is a non-incremental |
| # install, wipe the slate clean |
| adb.Delete(targetpath.join(app_dir, "native")) |
| |
| # From Android 28 onwards, `adb push` creates directories with insufficient |
| # permissions, resulting in errors when pushing files. `adb shell mkdir` |
| # works correctly however, so we create the directory here. |
| # See https://github.com/bazelbuild/examples/issues/77 for more information. |
| adb.Mkdir(targetpath.join(app_dir, "native")) |
| else: |
| # Otherwise, parse the manifest. Note that this branch is also taken if the |
| # manifest is empty. |
| for manifest_line in device_manifest.split("\n"): |
| if manifest_line: |
| name, checksum = manifest_line.split(" ") |
| device_checksums[name] = checksum |
| |
| libs_to_delete = set(device_checksums) - set(install_checksums) |
| libs_to_upload = set(install_checksums) - set(device_checksums) |
| common_libs = set(install_checksums).intersection(set(device_checksums)) |
| libs_to_upload.update([l for l in common_libs |
| if install_checksums[l] != device_checksums[l]]) |
| |
| libs_to_push = [(basename_to_path[lib], targetpath.join( |
| app_dir, "native", lib)) for lib in libs_to_upload] |
| |
| if not libs_to_delete and not libs_to_push and device_manifest is not None: |
| logging.info("Native libs up-to-date") |
| return |
| |
| num_files = len(libs_to_delete) + len(libs_to_push) |
| logging.info("Updating %d native lib%s...", |
| num_files, "s" if num_files != 1 else "") |
| |
| adb.Delete(targetpath.join(app_dir, "native", "native_manifest")) |
| |
| if libs_to_delete: |
| adb.DeleteMultiple( |
| [targetpath.join(app_dir, "native", lib) for lib in libs_to_delete]) |
| |
| upload_walltime_start = time.time() |
| fs = [adb.Push(local, remote) for local, remote in libs_to_push] |
| done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION) |
| upload_walltime = time.time() - upload_walltime_start |
| logging.debug("Native library upload walltime: %s seconds", upload_walltime) |
| |
| # If there is anything in not_done, then some adb call failed and we |
| # can cancel the rest. |
| if not_done: |
| for f in not_done: |
| f.cancel() |
| |
| # If any adb call resulted in an exception, re-raise it. |
| for f in done: |
| f.result() |
| |
| install_manifest = [ |
| six.ensure_str(name) + " " + checksum |
| for name, checksum in install_checksums.items() |
| ] |
| adb.PushString("\n".join(install_manifest), |
| targetpath.join(app_dir, "native", |
| "native_manifest")).result() |
| |
| |
| def VerifyInstallTimestamp(adb, app_package): |
| """Verifies that the app is unchanged since the last mobile-install.""" |
| expected_timestamp = adb.Pull( |
| targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp")) |
| if not expected_timestamp: |
| raise TimestampException( |
| "Cannot verify last mobile install. At least one non-incremental " |
| "'mobile-install' must precede incremental installs") |
| |
| actual_timestamp = adb.GetInstallTime(app_package) |
| if actual_timestamp is None: |
| raise TimestampException( |
| "Package '%s' is not installed on the device. At least one " |
| "non-incremental 'mobile-install' must precede incremental " |
| "installs." % app_package) |
| |
| if actual_timestamp != expected_timestamp: |
| raise TimestampException("Installed app '%s' has an unexpected timestamp. " |
| "Did you last install the app in a way other than " |
| "'mobile-install'?" % app_package) |
| |
| |
| def SplitIncrementalInstall(adb, app_package, execroot, split_main_apk, |
| split_apks): |
| """Does incremental installation using split packages.""" |
| app_dir = targetpath.join(DEVICE_DIRECTORY, app_package) |
| device_manifest_path = targetpath.join(app_dir, "split_manifest") |
| device_manifest = adb.Pull(device_manifest_path) |
| expected_timestamp = adb.Pull(targetpath.join(app_dir, "install_timestamp")) |
| actual_timestamp = adb.GetInstallTime(app_package) |
| device_checksums = {} |
| if device_manifest is not None: |
| for manifest_line in device_manifest.split("\n"): |
| if manifest_line: |
| name, checksum = manifest_line.split(" ") |
| device_checksums[name] = checksum |
| |
| install_checksums = {} |
| install_checksums["__MAIN__"] = Checksum( |
| hostpath.join(execroot, split_main_apk)) |
| for apk in split_apks: |
| install_checksums[apk] = Checksum(hostpath.join(execroot, apk)) |
| |
| reinstall_main = False |
| if (device_manifest is None or actual_timestamp is None or |
| actual_timestamp != expected_timestamp or |
| install_checksums["__MAIN__"] != device_checksums["__MAIN__"] or |
| set(device_checksums.keys()) != set(install_checksums.keys())): |
| # The main app is not up to date or not present or something happened |
| # with the on-device manifest. Start from scratch. Notably, we cannot |
| # uninstall a split package, so if the set of packages changes, we also |
| # need to do a full reinstall. |
| reinstall_main = True |
| device_checksums = {} |
| |
| apks_to_update = [ |
| apk for apk in split_apks if |
| apk not in device_checksums or |
| device_checksums[apk] != install_checksums[apk]] |
| |
| if not apks_to_update and not reinstall_main: |
| # Nothing to do |
| return |
| |
| # Delete the device manifest so that if something goes wrong, we do a full |
| # reinstall next time |
| adb.Delete(device_manifest_path) |
| |
| if reinstall_main: |
| logging.info("Installing main APK...") |
| adb.Uninstall(app_package) |
| adb.InstallMultiple(targetpath.join(execroot, split_main_apk)) |
| adb.PushString( |
| adb.GetInstallTime(app_package), |
| targetpath.join(app_dir, "install_timestamp")).result() |
| |
| logging.info("Reinstalling %s APKs...", len(apks_to_update)) |
| |
| for apk in apks_to_update: |
| adb.InstallMultiple(targetpath.join(execroot, apk), app_package) |
| |
| install_manifest = [ |
| six.ensure_str(name) + " " + checksum |
| for name, checksum in install_checksums.items() |
| ] |
| adb.PushString("\n".join(install_manifest), |
| targetpath.join(app_dir, "split_manifest")).result() |
| |
| |
| def IncrementalInstall(adb_path, |
| execroot, |
| stub_datafile, |
| output_marker, |
| adb_jobs, |
| start_type, |
| dexmanifest=None, |
| apk=None, |
| native_libs=None, |
| resource_apk=None, |
| split_main_apk=None, |
| split_apks=None, |
| user_home_dir=None, |
| extra_adb_args=None): |
| """Performs an incremental install. |
| |
| Args: |
| adb_path: Path to the adb executable. |
| execroot: Exec root. |
| stub_datafile: The stub datafile containing the app's package name. |
| output_marker: Path to the output marker file. |
| adb_jobs: The number of instances of adb to use in parallel. |
| start_type: A string describing whether/how to start the app after |
| installing it. Can be 'no', 'cold', or 'warm'. |
| dexmanifest: Path to the .dex manifest file. |
| apk: Path to the .apk file. May be None to perform an incremental install. |
| native_libs: Native libraries to install. |
| resource_apk: Path to the apk containing the app's resources. |
| split_main_apk: the split main .apk if split installation is desired. |
| split_apks: the list of split .apks to be installed. |
| user_home_dir: Path to the user's home directory. |
| extra_adb_args: Extra arguments that will always be passed to adb. |
| """ |
| temp_dir = tempfile.mkdtemp() |
| try: |
| adb = Adb(adb_path, temp_dir, adb_jobs, user_home_dir, extra_adb_args) |
| app_package = GetAppPackage(hostpath.join(execroot, stub_datafile)) |
| app_dir = targetpath.join(DEVICE_DIRECTORY, app_package) |
| if split_main_apk: |
| SplitIncrementalInstall(adb, app_package, execroot, split_main_apk, |
| split_apks) |
| else: |
| if not apk: |
| VerifyInstallTimestamp(adb, app_package) |
| |
| with open(hostpath.join(execroot, dexmanifest), "rb") as f: |
| dexmanifest = six.ensure_str(f.read(), "utf-8") |
| UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest, bool(apk)) |
| # TODO(ahumesky): UploadDexes waits for all the dexes to be uploaded, and |
| # then UploadResources is called. We could instead enqueue everything |
| # onto the threadpool so that uploading resources happens sooner. |
| UploadResources(adb, hostpath.join(execroot, resource_apk), app_dir) |
| UploadNativeLibs(adb, native_libs, app_dir, bool(apk)) |
| if apk: |
| apk_path = targetpath.join(execroot, apk) |
| adb.Install(apk_path) |
| future = adb.PushString( |
| adb.GetInstallTime(app_package), |
| targetpath.join(DEVICE_DIRECTORY, app_package, "install_timestamp")) |
| future.result() |
| else: |
| if start_type == "warm": |
| adb.StopAppAndSaveState(app_package) |
| else: |
| adb.StopApp(app_package) |
| |
| if start_type in ["cold", "warm", "debug"]: |
| logging.info("Starting application %s", app_package) |
| adb.StartApp(app_package, start_type) |
| |
| with open(output_marker, "wb") as _: |
| pass |
| except DeviceNotFoundError: |
| sys.exit("Error: Device not found") |
| except DeviceUnauthorizedError: |
| sys.exit("Error: Device unauthorized. Please check the confirmation " |
| "dialog on your device.") |
| except MultipleDevicesError as e: |
| sys.exit("Error: " + str(e) + "\nTry specifying a device serial with " |
| "\"bazel mobile-install --adb_arg=-s --adb_arg=$ANDROID_SERIAL\"") |
| except OldSdkException as e: |
| sys.exit("Error: The device does not support the API level specified in " |
| "the application's manifest. Check minSdkVersion in " |
| "AndroidManifest.xml") |
| except TimestampException as e: |
| sys.exit("Error:\n%s" % str(e)) |
| except AdbError as e: |
| sys.exit("Error:\n%s" % str(e)) |
| finally: |
| shutil.rmtree(temp_dir, True) |
| |
| |
| def main(unused_argv): |
| if FLAGS.verbosity == "1": # 'verbosity' flag is defined in absl.logging |
| level = logging.DEBUG |
| fmt = "%(levelname)-5s %(asctime)s %(module)s:%(lineno)3d] %(message)s" |
| else: |
| level = logging.INFO |
| fmt = "%(message)s" |
| logging.basicConfig(stream=sys.stdout, level=level, format=fmt) |
| |
| start_type = FLAGS.start |
| if FLAGS.start_app and start_type == "no": |
| start_type = "cold" |
| |
| IncrementalInstall( |
| adb_path=FLAGS.adb, |
| adb_jobs=FLAGS.adb_jobs, |
| execroot=FLAGS.execroot, |
| stub_datafile=FLAGS.stub_datafile, |
| output_marker=FLAGS.output_marker, |
| start_type=start_type, |
| native_libs=FLAGS.native_lib, |
| split_main_apk=FLAGS.split_main_apk, |
| split_apks=FLAGS.split_apk, |
| dexmanifest=FLAGS.dexmanifest, |
| apk=FLAGS.apk, |
| resource_apk=FLAGS.resource_apk, |
| user_home_dir=FLAGS.user_home_dir, |
| extra_adb_args=FLAGS.extra_adb_arg) |
| |
| |
| if __name__ == "__main__": |
| FLAGS(sys.argv) |
| # process any additional flags in --flagfile |
| if FLAGS.flagfile: |
| with open(FLAGS.flagfile, "rb") as flagsfile: |
| FLAGS.Reset() |
| FLAGS(sys.argv + [line.strip() for line in flagsfile.readlines()]) |
| |
| app.run(main) |