Make the Android incremental installer script support incrementally installing native libraries. -- MOS_MIGRATED_REVID=99257598
diff --git a/tools/android/incremental_install.py b/tools/android/incremental_install.py index ad2f904..9862f7a 100644 --- a/tools/android/incremental_install.py +++ b/tools/android/incremental_install.py
@@ -33,6 +33,7 @@ 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") @@ -171,6 +172,11 @@ "non-incremental 'mobile-install' must precede incremental " "installs." % package) + 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]) @@ -436,6 +442,90 @@ 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 UploadNativeLibs(adb, native_lib_args, app_dir): + """Uploads native libraries to the device.""" + + native_libs = ConvertNativeLibs(native_lib_args) + libs = set() + if native_libs: + abi = adb.GetAbi() + if abi not in native_libs: + logging.warn("No native libs for device ABI '%s'. Available ABIs: %s", + abi, ", ".join(native_libs)) + else: + 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 = adb.Pull("%s/native/native_manifest" % app_dir) + device_checksums = {} + if device_manifest: + for name, checksum in [l.split(" ") for l in device_manifest.split("\n")]: + 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" % ( @@ -454,7 +544,8 @@ def IncrementalInstall(adb_path, execroot, stub_datafile, output_marker, adb_jobs, start_app, dexmanifest=None, apk=None, - resource_apk=None, split_main_apk=None, split_apks=None, + native_libs=None, resource_apk=None, + split_main_apk=None, split_apks=None, user_home_dir=None): """Performs an incremental install. @@ -467,6 +558,7 @@ start_app: If True, starts the app after updating. 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. @@ -480,6 +572,8 @@ if split_main_apk: adb.InstallMultiple(os.path.join(execroot, split_main_apk)) for split_apk in split_apks: + # TODO(build-team): This always reinstalls everything, which defeats the + # purpose of this whole system. adb.InstallMultiple(os.path.join(execroot, split_apk), app_package) else: if not apk: @@ -492,6 +586,7 @@ # 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) if apk: apk_path = os.path.join(execroot, apk) adb.Install(apk_path) @@ -542,6 +637,7 @@ stub_datafile=FLAGS.stub_datafile, output_marker=FLAGS.output_marker, start_app=FLAGS.start_app, + native_libs=FLAGS.native_lib, split_main_apk=FLAGS.split_main_apk, split_apks=FLAGS.split_apk, dexmanifest=FLAGS.dexmanifest,
diff --git a/tools/android/incremental_install_test.py b/tools/android/incremental_install_test.py index b8ea64f..a040f74 100644 --- a/tools/android/incremental_install_test.py +++ b/tools/android/incremental_install_test.py
@@ -78,6 +78,8 @@ elif shell_cmdln.startswith("rm"): file_path = shell_cmdln.split()[2] self.files.pop(file_path, None) + elif shell_cmdln.startswith("getprop ro.product.cpu.abi"): + return self._CreatePopenMock(0, "armeabi-v7a", "") else: raise Exception("Unknown shell command line: %s" % shell_cmdln) # Return a mock subprocess.Popen object @@ -151,11 +153,13 @@ def _PutDeviceFile(self, f, content): self._mock_adb.files[self._GetDeviceAppPath(f)] = content - def _CallIncrementalInstall(self, incremental, start_app=False): + def _CallIncrementalInstall(self, incremental, native_libs=None, + start_app=False): if incremental: apk = None else: apk = self._APK + incremental_install.IncrementalInstall( adb_path=self._ADB_PATH, execroot=self._EXEC_ROOT, @@ -163,13 +167,13 @@ dexmanifest=self._DEXMANIFEST, apk=apk, resource_apk=self._RESOURCE_APK, + native_libs=native_libs, output_marker=self._OUTPUT_MARKER, adb_jobs=1, start_app=start_app, user_home_dir="/home/root") def testUploadToPristineDevice(self): - self._CreateZip() with open("dex1", "w") as f: @@ -190,8 +194,35 @@ self.assertEquals("content3", self._GetDeviceFile("dex/ip3")) self.assertEquals("resource apk", self._GetDeviceFile("resources.ap_")) - def testUploadWithOneChangedFile(self): + def testUploadNativeLibs(self): + self._CreateZip() + with open("liba.so", "w") as f: + f.write("liba_1") + with open("libb.so", "w") as f: + f.write("libb_1") + native_libs = ["armeabi-v7a:liba.so", "armeabi-v7a:libb.so"] + self._CallIncrementalInstall(incremental=False, native_libs=native_libs) + self.assertEquals("liba_1", self._GetDeviceFile("native/liba.so")) + self.assertEquals("libb_1", self._GetDeviceFile("native/libb.so")) + + # Change a library + with open("libb.so", "w") as f: + f.write("libb_2") + self._CallIncrementalInstall(incremental=True, native_libs=native_libs) + self.assertEquals("libb_2", self._GetDeviceFile("native/libb.so")) + + # Delete a library + self._CallIncrementalInstall( + incremental=True, native_libs=["armeabi-v7a:liba.so"]) + self.assertFalse( + self._GetDeviceAppPath("native/libb.so") in self._mock_adb.files) + + # Add the deleted library back + self._CallIncrementalInstall(incremental=True, native_libs=native_libs) + self.assertEquals("libb_2", self._GetDeviceFile("native/libb.so")) + + def testUploadWithOneChangedFile(self): # Existing manifest from a previous install. self._CreateRemoteManifest( "zip1 zp1 ip1 0",