From 014d1ff14a94667827f6f59341a9394896f51d10 Mon Sep 17 00:00:00 2001 From: "cjhopman@chromium.org" Date: Tue, 2 Jul 2013 01:52:33 +0000 Subject: [PATCH] [Android] Add an action to check/record attached devices When doing a gyp_managed_install, we install APKs to the attached device. Currently this can fail in many ways (no device attached, multiple devices attached, device offline, device doesn't have root, etc.). In addition, we need to detect changes to the attached device (particularly when the device is switched, when an APK is uninstalled/updated). The current approach is to check all this information in the action interacting with the device. This means that when there is some problem we print the same warning messages for every APK that is built, and, in some cases, multiple times for each APK. Also, we have to run every install/push action every build because we detect changes to the attached device in that action. This change creates a new build action, "get device configurations". This action inspects the attached devices, filters out offline devices, filters out devices without root, and then writes a configuration file with the id+metadata for the first non-filtered device. This configuration is then used by each of the build steps that interacts with the device. This consolidates all the device checking to a single place, and the build actions don't need to do any checking. In addition, to detect changes in the attached device, we only need to run this single action every build and the install/push actions will only change when the device/metadata changes. Also, with this change we can now gracefully handle the case where multiple devices are attached (currently just write the configuration for the first valid device and install to that one). Review URL: https://chromiumcodereview.appspot.com/16831013 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@209582 0039d316-1c4b-4281-b951-d872f2087c98 --- build/android/gyp/apk_install.py | 45 ++++----- .../gyp/create_device_library_links.py | 25 ++--- build/android/gyp/get_device_configuration.py | 75 ++++++++++++++ build/android/gyp/push_libraries.py | 26 +++-- build/android/gyp/util/build_device.py | 98 +++++++++++++++++++ build/android/push_libraries.gypi | 7 +- build/android/setup.gyp | 31 +++++- build/common.gypi | 4 + build/java_apk.gypi | 12 +-- 9 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 build/android/gyp/get_device_configuration.py create mode 100644 build/android/gyp/util/build_device.py diff --git a/build/android/gyp/apk_install.py b/build/android/gyp/apk_install.py index ef10c6a3bd6954..f0ed973a3085f1 100755 --- a/build/android/gyp/apk_install.py +++ b/build/android/gyp/apk_install.py @@ -14,20 +14,18 @@ import subprocess import sys +from util import build_device from util import build_utils from util import md5_check BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..') sys.path.append(BUILD_ANDROID_DIR) -from pylib import android_commands from pylib.utils import apk_helper - -def GetMetadata(apk_package): +def GetNewMetadata(device, apk_package): """Gets the metadata on the device for the apk_package apk.""" - adb = android_commands.AndroidCommands() - output = adb.RunShellCommand('ls -l /data/app/') + output = device.RunShellCommand('ls -l /data/app/') # Matches lines like: # -rw-r--r-- system system 7376582 2013-04-19 16:34 org.chromium.chrome.testshell.apk # -rw-r--r-- system system 7376582 2013-04-19 16:34 org.chromium.chrome.testshell-1.apk @@ -35,23 +33,19 @@ def GetMetadata(apk_package): matches = filter(apk_matcher, output) return matches[0] if matches else None - -def HasInstallMetadataChanged(apk_package, metadata_path): +def HasInstallMetadataChanged(device, apk_package, metadata_path): """Checks if the metadata on the device for apk_package has changed.""" if not os.path.exists(metadata_path): return True with open(metadata_path, 'r') as expected_file: - return expected_file.read() != GetMetadata(apk_package) + return expected_file.read() != device.GetInstallMetadata(apk_package) -def RecordInstallMetadata(apk_package, metadata_path): +def RecordInstallMetadata(device, apk_package, metadata_path): """Records the metadata from the device for apk_package.""" - metadata = GetMetadata(apk_package) + metadata = GetNewMetadata(device, apk_package) if not metadata: - if not android_commands.AndroidCommands().IsRootEnabled(): - raise Exception('APK install failed unexpectedly -- root not enabled on ' - 'the device (run adb root).') raise Exception('APK install failed unexpectedly.') with open(metadata_path, 'w') as outfile: @@ -59,11 +53,6 @@ def RecordInstallMetadata(apk_package, metadata_path): def main(argv): - if not build_utils.IsDeviceReady(): - build_utils.PrintBigWarning( - 'Zero (or multiple) devices attached. Skipping APK install.') - return - parser = optparse.OptionParser() parser.add_option('--android-sdk-tools', help='Path to Android SDK tools.') @@ -71,28 +60,29 @@ def main(argv): help='Path to .apk to install.') parser.add_option('--install-record', help='Path to install record (touched only when APK is installed).') + parser.add_option('--build-device-configuration', + help='Path to build device configuration.') parser.add_option('--stamp', help='Path to touch on success.') options, _ = parser.parse_args() - # TODO(cjhopman): Should this install to all devices/be configurable? - install_cmd = [ - os.path.join(options.android_sdk_tools, 'adb'), - 'install', '-r', - options.apk_path] + device = build_device.GetBuildDeviceFromPath( + options.build_device_configuration) + if not device: + return - serial_number = android_commands.AndroidCommands().Adb().GetSerialNumber() + serial_number = device.GetSerialNumber() apk_package = apk_helper.GetPackageName(options.apk_path) metadata_path = '%s.%s.device.time.stamp' % (options.apk_path, serial_number) # If the APK on the device does not match the one that was last installed by # the build, then the APK has to be installed (regardless of the md5 record). - force_install = HasInstallMetadataChanged(apk_package, metadata_path) + force_install = HasInstallMetadataChanged(device, apk_package, metadata_path) def Install(): - build_utils.CheckCallDie(install_cmd) - RecordInstallMetadata(apk_package, metadata_path) + device.Install(options.apk_path, reinstall=True) + RecordInstallMetadata(device, apk_package, metadata_path) build_utils.Touch(options.install_record) @@ -101,7 +91,6 @@ def Install(): Install, record_path=record_path, input_paths=[options.apk_path], - input_strings=install_cmd, force=force_install) if options.stamp: diff --git a/build/android/gyp/create_device_library_links.py b/build/android/gyp/create_device_library_links.py index 5dd5f39988c220..1df177d6dd1366 100755 --- a/build/android/gyp/create_device_library_links.py +++ b/build/android/gyp/create_device_library_links.py @@ -16,17 +16,17 @@ import os import sys +from util import build_device from util import build_utils from util import md5_check BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..') sys.path.append(BUILD_ANDROID_DIR) -from pylib import android_commands from pylib.utils import apk_helper -def RunShellCommand(adb, cmd): - output = adb.RunShellCommand(cmd) +def RunShellCommand(device, cmd): + output = device.RunShellCommand(cmd) if output: raise Exception( @@ -53,15 +53,19 @@ def CreateSymlinkScript(options): def TriggerSymlinkScript(options): + device = build_device.GetBuildDeviceFromPath( + options.build_device_configuration) + if not device: + return + apk_package = apk_helper.GetPackageName(options.apk) apk_libraries_dir = '/data/data/%s/lib' % apk_package - adb = android_commands.AndroidCommands() device_dir = os.path.dirname(options.script_device_path) mkdir_cmd = ('if [ ! -e %(dir)s ]; then mkdir -p %(dir)s; fi ' % { 'dir': device_dir }) - RunShellCommand(adb, mkdir_cmd) - adb.PushIfNeeded(options.script_host_path, options.script_device_path) + RunShellCommand(device, mkdir_cmd) + device.PushIfNeeded(options.script_host_path, options.script_device_path) trigger_cmd = ( 'APK_LIBRARIES_DIR=%(apk_libraries_dir)s; ' @@ -72,15 +76,10 @@ def TriggerSymlinkScript(options): 'target_dir': options.target_dir, 'script_device_path': options.script_device_path } - RunShellCommand(adb, trigger_cmd) + RunShellCommand(device, trigger_cmd) def main(argv): - if not build_utils.IsDeviceReady(): - build_utils.PrintBigWarning( - 'Zero (or multiple) devices attached. Skipping creating symlinks.') - return - parser = optparse.OptionParser() parser.add_option('--apk', help='Path to the apk.') parser.add_option('--script-host-path', @@ -92,6 +91,8 @@ def main(argv): parser.add_option('--target-dir', help='Device directory that contains the target libraries for symlinks.') parser.add_option('--stamp', help='Path to touch on success.') + parser.add_option('--build-device-configuration', + help='Path to build device configuration.') options, _ = parser.parse_args() required_options = ['apk', 'libraries_json', 'script_host_path', diff --git a/build/android/gyp/get_device_configuration.py b/build/android/gyp/get_device_configuration.py new file mode 100644 index 00000000000000..f27c12be4b360a --- /dev/null +++ b/build/android/gyp/get_device_configuration.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Gets and writes the configurations of the attached devices. + +This configuration is used by later build steps to determine which devices to +install to and what needs to be installed to those devices. +""" + +import logging +import optparse +import os +import subprocess +import sys + +from util import build_utils +from util import build_device + +BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..') +sys.path.append(BUILD_ANDROID_DIR) + +from pylib.utils import apk_helper + + +def main(argv): + parser = optparse.OptionParser() + parser.add_option('--stamp', action='store') + parser.add_option('--output', action='store') + options, _ = parser.parse_args(argv) + + devices = build_device.GetAttachedDevices() + + device_configurations = [] + for d in devices: + configuration, is_online, has_root = ( + build_device.GetConfigurationForDevice(d)) + + if not is_online: + build_utils.PrintBigWarning( + '%s is not online. Skipping managed install for this device. ' + 'Try rebooting the device to fix this warning.' % d) + continue + + if not has_root: + build_utils.PrintBigWarning( + '"adb root" failed on device: %s\n' + 'Skipping managed install for this device.' + % configuration['description']) + continue + + device_configurations.append(configuration) + + if len(device_configurations) == 0: + build_utils.PrintBigWarning( + 'No valid devices attached. Skipping managed install steps.') + elif len(devices) > 1: + # Note that this checks len(devices) and not len(device_configurations). + # This way, any time there are multiple devices attached it is + # explicitly stated which device we will install things to even if all but + # one device were rejected for other reasons (e.g. two devices attached, + # one w/o root). + build_utils.PrintBigWarning( + 'Multiple devices attached. ' + 'Installing to the preferred device: ' + '%(id)s (%(description)s)' % (device_configurations[0])) + + + build_device.WriteConfigurations(device_configurations, options.output) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/build/android/gyp/push_libraries.py b/build/android/gyp/push_libraries.py index b94d6b5ebf800e..349e0cbafc621c 100755 --- a/build/android/gyp/push_libraries.py +++ b/build/android/gyp/push_libraries.py @@ -13,20 +13,19 @@ import os import sys +from util import build_device from util import build_utils from util import md5_check -BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..') -sys.path.append(BUILD_ANDROID_DIR) - -from pylib import android_commands - - def DoPush(options): libraries = build_utils.ReadJson(options.libraries_json) - adb = android_commands.AndroidCommands() - serial_number = adb.Adb().GetSerialNumber() + device = build_device.GetBuildDeviceFromPath( + options.build_device_configuration) + if not device: + return + + serial_number = device.GetSerialNumber() # A list so that it is modifiable in Push below. needs_directory = [True] for lib in libraries: @@ -35,9 +34,9 @@ def DoPush(options): def Push(): if needs_directory: - adb.RunShellCommand('mkdir -p ' + options.device_dir) + device.RunShellCommand('mkdir -p ' + options.device_dir) needs_directory[:] = [] # = False - adb.PushIfNeeded(host_path, device_path) + device.PushIfNeeded(host_path, device_path) record_path = '%s.%s.push.md5.stamp' % (host_path, serial_number) md5_check.CallAndRecordIfStale( @@ -48,11 +47,6 @@ def Push(): def main(argv): - if not build_utils.IsDeviceReady(): - build_utils.PrintBigWarning( - 'Zero (or multiple) devices attached. Skipping native library push.') - return - parser = optparse.OptionParser() parser.add_option('--libraries-dir', help='Directory that contains stripped libraries.') @@ -61,6 +55,8 @@ def main(argv): parser.add_option('--libraries-json', help='Path to the json list of native libraries.') parser.add_option('--stamp', help='Path to touch on success.') + parser.add_option('--build-device-configuration', + help='Path to build device configuration.') options, _ = parser.parse_args() required_options = ['libraries_dir', 'device_dir', 'libraries_json'] diff --git a/build/android/gyp/util/build_device.py b/build/android/gyp/util/build_device.py new file mode 100644 index 00000000000000..11f6277453ba67 --- /dev/null +++ b/build/android/gyp/util/build_device.py @@ -0,0 +1,98 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" A simple device interface for build steps. + +""" + +import logging +import os +import re +import sys + +import build_utils + +BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..', '..') +sys.path.append(BUILD_ANDROID_DIR) + +from pylib import android_commands + +from pylib.android_commands import GetAttachedDevices + + +class BuildDevice(object): + def __init__(self, configuration): + self.id = configuration['id'] + self.description = configuration['description'] + self.install_metadata = configuration['install_metadata'] + self.adb = android_commands.AndroidCommands(self.id) + + def RunShellCommand(self, *args, **kwargs): + return self.adb.RunShellCommand(*args, **kwargs) + + def PushIfNeeded(self, *args, **kwargs): + return self.adb.PushIfNeeded(*args, **kwargs) + + def GetSerialNumber(self): + return self.id + + def Install(self, *args, **kwargs): + return self.adb.Install(*args, **kwargs) + + def GetInstallMetadata(self, apk_package): + """Gets the metadata on the device for the apk_package apk.""" + # Matches lines like: + # -rw-r--r-- system system 7376582 2013-04-19 16:34 \ + # org.chromium.chrome.testshell.apk + # -rw-r--r-- system system 7376582 2013-04-19 16:34 \ + # org.chromium.chrome.testshell-1.apk + apk_matcher = lambda s: re.match('.*%s(-[0-9]*)?.apk$' % apk_package, s) + matches = filter(apk_matcher, self.install_metadata) + return matches[0] if matches else None + + +def GetConfigurationForDevice(id): + adb = android_commands.AndroidCommands(id) + configuration = None + has_root = False + is_online = adb.IsOnline() + if is_online: + cmd = 'ls -l /data/app; getprop ro.build.description' + cmd_output = adb.RunShellCommand(cmd) + has_root = not 'Permission denied' in cmd_output[0] + if not has_root: + # Disable warning log messages from EnableAdbRoot() + logging.getLogger().disabled = True + has_root = adb.EnableAdbRoot() + logging.getLogger().disabled = False + cmd_output = adb.RunShellCommand(cmd) + + configuration = { + 'id': id, + 'description': cmd_output[-1], + 'install_metadata': cmd_output[:-1], + } + return configuration, is_online, has_root + + +def WriteConfigurations(configurations, path): + # Currently we only support installing to the first device. + build_utils.WriteJson(configurations[:1], path, only_if_changed=True) + + +def ReadConfigurations(path): + return build_utils.ReadJson(path) + + +def GetBuildDevice(configurations): + assert len(configurations) == 1 + return BuildDevice(configurations[0]) + + +def GetBuildDeviceFromPath(path): + configurations = ReadConfigurations(path) + if len(configurations) > 0: + return GetBuildDevice(ReadConfigurations(path)) + return None + diff --git a/build/android/push_libraries.gypi b/build/android/push_libraries.gypi index 17f479debbef31..1f17660c44832d 100644 --- a/build/android/push_libraries.gypi +++ b/build/android/push_libraries.gypi @@ -29,17 +29,14 @@ '<(DEPTH)/build/android/gyp/util/md5_check.py', '<(DEPTH)/build/android/gyp/push_libraries.py', '<(strip_stamp)', + '<(build_device_config_path)', ], 'outputs': [ '<(push_stamp)', - # If a user switches the connected device, new libraries may - # need to be pushed even if there have been no changes. To - # ensure that the libraries on the device are always - # up-to-date, this step should always be triggered. - '<(push_stamp).fake', ], 'action': [ 'python', '<(DEPTH)/build/android/gyp/push_libraries.py', + '--build-device-configuration=<(build_device_config_path)', '--libraries-dir=<(libraries_source_dir)', '--device-dir=<(device_library_dir)', '--libraries-json=<(ordered_libraries_file)', diff --git a/build/android/setup.gyp b/build/android/setup.gyp index 9abfbc6cf341db..7dce19de7266a0 100644 --- a/build/android/setup.gyp +++ b/build/android/setup.gyp @@ -25,6 +25,27 @@ }], ], 'targets': [ + { + 'target_name': 'get_build_device_configurations', + 'type': 'none', + 'actions': [ + { + 'action_name': 'get configurations', + 'inputs': [ + 'gyp/util/build_device.py', + 'gyp/get_device_configuration.py', + ], + 'outputs': [ + '<(build_device_config_path)', + '<(build_device_config_path).fake', + ], + 'action': [ + 'python', 'gyp/get_device_configuration.py', + '--output=<(build_device_config_path)', + ], + } + ], + }, { # Target for creating common output build directories. Creating output # dirs beforehand ensures that build scripts can assume these folders to @@ -37,11 +58,11 @@ { 'action_name': 'create_java_output_dirs', 'variables' : { - 'output_dirs' : [ - '<(PRODUCT_DIR)/apks', - '<(PRODUCT_DIR)/lib.java', - '<(PRODUCT_DIR)/test.lib.java', - ] + 'output_dirs' : [ + '<(PRODUCT_DIR)/apks', + '<(PRODUCT_DIR)/lib.java', + '<(PRODUCT_DIR)/test.lib.java', + ] }, 'inputs' : [], # By not specifying any outputs, we ensure that this command isn't diff --git a/build/common.gypi b/build/common.gypi index 75cb12e9d28d49..12ada0e96c045a 100644 --- a/build/common.gypi +++ b/build/common.gypi @@ -1087,6 +1087,10 @@ 'android_app_version_name%': 'Developer Build', 'android_app_version_code%': 0, + + # Contains data about the attached devices for gyp_managed_install. + 'build_device_config_path': '<(PRODUCT_DIR)/build_devices.cfg', + 'sas_dll_exists': '