|  | # 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.""" | 
|  |  | 
|  | 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 | 
|  |  | 
|  | 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) | 
|  | raw_stdout, raw_stderr = adb.communicate() | 
|  | # This hackery is to account for a change in what communicate() returns | 
|  | # in Python 3.7.  We just deal with it being either string or bytes. | 
|  | if isinstance(raw_stderr, bytes): | 
|  | stdout = raw_stdout.decode("utf-8").strip() | 
|  | stderr = raw_stderr.decode("utf-8").strip() | 
|  | else: | 
|  | stdout = raw_stdout.strip() | 
|  | stderr = raw_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. | 
|  | 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=(.*)$", 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 uninterpreted contents of a file or None if the file didn't exist. | 
|  | """ | 
|  | local = self._CreateLocalFile() | 
|  | try: | 
|  | self._Exec(["pull", remote, local]) | 
|  | # Subtle stuff here.  We read the file as a blob of bytes, which is bytes | 
|  | # in python3, but want to return it as a str, so we do a no-op decode. | 
|  | # It is up to the caller to re-decode the content if they are reading | 
|  | # a text file that is really UTF-8. | 
|  | # FWIW: Earlier code decoded the content as if it were UTF-8, which is | 
|  | # arguably wrong. This tool sometimes pulls text files and sometimes | 
|  | # binaries. The caller should specify if they want it decoded or not. | 
|  | with open(local, "rb") as f: | 
|  | return "".join([chr(b) for b in f.read()]) | 
|  | 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, "r", encoding="utf-8") as f: | 
|  | return f.readlines()[1].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 = 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 = [ | 
|  | 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 = [ | 
|  | 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), "r", | 
|  | encoding="utf-8") as f: | 
|  | dexmanifest_content = f.read() | 
|  | UploadDexes(adb, execroot, app_dir, temp_dir, dexmanifest_content, | 
|  | 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) |