| # 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 | |
| import hashlib | |
| import logging | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import time | |
| import zipfile | |
| from third_party.py import gflags | |
| from third_party.py.concurrent import futures | |
| gflags.DEFINE_string("split_main_apk", None, "The main APK for split install") | |
| gflags.DEFINE_multistring("split_apk", [], "Split APKs to install") | |
| gflags.DEFINE_string("dexmanifest", None, "The .dex manifest") | |
| gflags.DEFINE_multistring("native_lib", None, "Native libraries to install") | |
| gflags.DEFINE_string("resource_apk", None, "The resource .apk") | |
| gflags.DEFINE_string("apk", None, "The app .apk. If not specified, " | |
| "do incremental deployment") | |
| gflags.DEFINE_string("adb", None, "ADB to use") | |
| gflags.DEFINE_string("stub_datafile", None, "The stub data file") | |
| gflags.DEFINE_string("output_marker", None, "The output marker file") | |
| gflags.DEFINE_multistring("extra_adb_arg", [], "Extra arguments to adb") | |
| gflags.DEFINE_string("execroot", ".", "The exec root") | |
| gflags.DEFINE_integer("adb_jobs", 2, | |
| "The number of instances of adb to use in parallel to " | |
| "update files on the device", | |
| lower_bound=1) | |
| gflags.DEFINE_enum("start", "no", ["no", "cold", "warm"], "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.") | |
| gflags.DEFINE_boolean("start_app", False, "Deprecated, use 'start'.") | |
| gflags.DEFINE_string("user_home_dir", None, "Path to the user's home directory") | |
| gflags.DEFINE_string("flagfile", None, | |
| "Path to a file to read additional flags from") | |
| gflags.DEFINE_string("verbosity", None, "Logging verbosity") | |
| FLAGS = gflags.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 Adb(object): | |
| """A class to handle interaction with adb.""" | |
| def __init__(self, adb_path, temp_dir, adb_jobs, user_home_dir): | |
| 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) | |
| def _Exec(self, adb_args): | |
| """Executes the given adb command + args.""" | |
| args = [self._adb_path] + FLAGS.extra_adb_arg + 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 | |
| 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. | |
| 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 = os.path.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 file(local, "w") as f: | |
| f.write(contents) | |
| 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 file(local) as f: | |
| return 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): | |
| """Starts the app with the given 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]) | |
| 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 file(stub_datafile) 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 = os.path.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("%s/manifest" % dex_dir) | |
| 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("%s/*" % 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("%s/manifest" % dex_dir) | |
| # 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 = os.path.join(temp_dir, "dex", str(i)) | |
| with zipfile.ZipFile(os.path.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( | |
| (os.path.join(dexzip_tempdir, zippath), "%s/%s" % (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, "%s/%s" % (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(os.path.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, "%s/manifest" % dex_dir).result() | |
| def Checksum(filename): | |
| """Compute the SHA-256 checksum of a file.""" | |
| h = hashlib.sha256() | |
| with file(filename, "r") 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 = "%s/%s" % (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, "%s/%s" % (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(), 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("%s/native/native_manifest" % app_dir) | |
| 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("%s/native" % app_dir) | |
| 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], "%s/native/%s" % (app_dir, 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("%s/native/native_manifest" % app_dir) | |
| if libs_to_delete: | |
| adb.DeleteMultiple([ | |
| "%s/native/%s" % (app_dir, 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.iteritems()] | |
| adb.PushString("\n".join(install_manifest), | |
| "%s/native/native_manifest" % app_dir).result() | |
| def VerifyInstallTimestamp(adb, app_package): | |
| """Verifies that the app is unchanged since the last mobile-install.""" | |
| expected_timestamp = adb.Pull("%s/%s/install_timestamp" % ( | |
| DEVICE_DIRECTORY, app_package)) | |
| 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 = os.path.join(DEVICE_DIRECTORY, app_package) | |
| device_manifest_path = "%s/split_manifest" % app_dir | |
| device_manifest = adb.Pull(device_manifest_path) | |
| expected_timestamp = adb.Pull("%s/install_timestamp" % app_dir) | |
| 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( | |
| os.path.join(execroot, split_main_apk)) | |
| for apk in split_apks: | |
| install_checksums[apk] = Checksum(os.path.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(os.path.join(execroot, split_main_apk)) | |
| adb.PushString( | |
| adb.GetInstallTime(app_package), | |
| "%s/install_timestamp" % app_dir).result() | |
| logging.info("Reinstalling %s APKs...", len(apks_to_update)) | |
| for apk in apks_to_update: | |
| adb.InstallMultiple(os.path.join(execroot, apk), app_package) | |
| install_manifest = [ | |
| name + " " + checksum for name, checksum in install_checksums.iteritems()] | |
| adb.PushString("\n".join(install_manifest), | |
| "%s/split_manifest" % app_dir).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): | |
| """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. | |
| """ | |
| temp_dir = tempfile.mkdtemp() | |
| try: | |
| adb = Adb(adb_path, temp_dir, adb_jobs, user_home_dir) | |
| app_package = GetAppPackage(os.path.join(execroot, stub_datafile)) | |
| app_dir = os.path.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 file(os.path.join(execroot, dexmanifest)) as f: | |
| dexmanifest = f.read() | |
| 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, os.path.join(execroot, resource_apk), app_dir) | |
| UploadNativeLibs(adb, native_libs, app_dir, bool(apk)) | |
| if apk: | |
| apk_path = os.path.join(execroot, apk) | |
| adb.Install(apk_path) | |
| future = adb.PushString( | |
| adb.GetInstallTime(app_package), | |
| "%s/%s/install_timestamp" % (DEVICE_DIRECTORY, app_package)) | |
| future.result() | |
| else: | |
| if start_type == "warm": | |
| adb.StopAppAndSaveState(app_package) | |
| else: | |
| adb.StopApp(app_package) | |
| if start_type in ["cold", "warm"]: | |
| logging.info("Starting application %s", app_package) | |
| adb.StartApp(app_package) | |
| with file(output_marker, "w") 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: " + e.message + "\nTry specifying a device serial with " | |
| "\"blaze 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" % e.message) | |
| except AdbError as e: | |
| sys.exit("Error:\n%s" % e.message) | |
| finally: | |
| shutil.rmtree(temp_dir, True) | |
| def main(): | |
| if FLAGS.verbosity == "1": | |
| 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) | |
| if __name__ == "__main__": | |
| FLAGS(sys.argv) | |
| # process any additional flags in --flagfile | |
| if FLAGS.flagfile: | |
| with open(FLAGS.flagfile) as flagsfile: | |
| FLAGS.Reset() | |
| FLAGS(sys.argv + [line.strip() for line in flagsfile.readlines()]) | |
| main() |