#!/usr/bin/env python # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. """ Find an Android device with a given ABI. The name of the Android device is printed to stdout. Optionally configure and launch an emulator if there's no existing device for a given ABI. Will download and install Android SDK components as needed. """ import optparse import os import re import sys import traceback import utils DEBUG = False VERBOSE = False def BuildOptions(): result = optparse.OptionParser() result.add_option( "-a", "--abi", action="store", type="string", help="Desired ABI. armeabi-v7a or x86.") result.add_option( "-b", "--bootstrap", help='Bootstrap - create an emulator, installing SDK packages if needed.', default=False, action="store_true") result.add_option( "-d", "--debug", help='Turn on debugging diagnostics.', default=False, action="store_true") result.add_option( "-v", "--verbose", help='Verbose output.', default=False, action="store_true") return result def ProcessOptions(options): global DEBUG DEBUG = options.debug global VERBOSE VERBOSE = options.verbose if options.abi is None: sys.stderr.write('--abi not specified.\n') return False return True def ParseAndroidListSdkResult(text): """ Parse the output of an 'android list sdk' command. Return list of (id-num, id-key, type, description). """ header_regex = re.compile( r'Packages available for installation or update: \d+\n') packages = re.split(header_regex, text) if len(packages) != 2: raise utils.Error("Could not get a list of packages to install") entry_regex = re.compile( r'^id\: (\d+) or "([^"]*)"\n\s*Type\: ([^\n]*)\n\s*Desc\: (.*)') entries = [] for entry in packages[1].split('----------\n'): match = entry_regex.match(entry) if match == None: continue entries.append((int(match.group(1)), match.group(2), match.group(3), match.group(4))) return entries def AndroidListSdk(): return ParseAndroidListSdkResult(utils.RunCommand( ["android", "list", "sdk", "-a", "-e"])) def AndroidSdkFindPackage(packages, key): """ Args: packages: list of (id-num, id-key, type, description). key: (id-key, type, description-prefix). """ (key_id, key_type, key_description_prefix) = key for package in packages: (package_num, package_id, package_type, package_description) = package if (package_id == key_id and package_type == key_type and package_description.startswith(key_description_prefix)): return package return None def EnsureSdkPackageInstalled(packages, key): """ Makes sure the package with a given key is installed. key is (id-key, type, description-prefix) Returns True if the package was not already installed. """ entry = AndroidSdkFindPackage(packages, key) if entry is None: raise utils.Error("Could not find a package for key %s" % key) packageId = entry[0] if VERBOSE: sys.stderr.write('Checking Android SDK package %s...\n' % str(entry)) out = utils.RunCommand( ["android", "update", "sdk", "-a", "-u", "--filter", str(packageId)]) return '\nInstalling Archives:\n' in out def SdkPackagesForAbi(abi): packagesForAbi = { 'armeabi-v7a': [ # The platform needed to install the armeabi ABI system image: ('android-15', 'Platform', 'Android SDK Platform 4.0.3'), # The armeabi-v7a ABI system image: ('sysimg-15', 'SystemImage', 'Android SDK Platform 4.0.3') ], 'x86': [ # The platform needed to install the x86 ABI system image: ('android-15', 'Platform', 'Android SDK Platform 4.0.3'), # The x86 ABI system image: ('sysimg-15', 'SystemImage', 'Android SDK Platform 4.0.4') ] } if abi not in packagesForAbi: raise utils.Error('Unsupported abi %s' % abi) return packagesForAbi[abi] def TargetForAbi(abi): for package in SdkPackagesForAbi(abi): if package[1] == 'Platform': return package[0] def EnsureAndroidSdkPackagesInstalled(abi): """Return true if at least one package was not already installed.""" abiPackageList = SdkPackagesForAbi(abi) installedSomething = False packages = AndroidListSdk() for package in abiPackageList: installedSomething |= EnsureSdkPackageInstalled(packages, package) return installedSomething def ParseAndroidListAvdResult(text): """ Parse the output of an 'android list avd' command. Return List of {Name: Path: Target: ABI: Skin: Sdcard:} """ text = text.split('Available Android Virtual Devices:\n')[-1] text = text.split( 'The following Android Virtual Devices could not be loaded:\n')[0] result = [] line_re = re.compile(r'^\s*([^\:]+)\:\s*(.*)$') for chunk in text.split('\n---------\n'): entry = {} for line in chunk.split('\n'): line = line.strip() if len(line) == 0: continue match = line_re.match(line) if match is None: sys.stderr.write('Match fail %s\n' % str(line)) continue #raise utils.Error('Match failed') entry[match.group(1)] = match.group(2) if len(entry) > 0: result.append(entry) return result def AndroidListAvd(): """Returns a list of available Android Virtual Devices.""" return ParseAndroidListAvdResult(utils.RunCommand(["android", "list", "avd"])) def FindAvd(avds, key): for avd in avds: if avd['Name'] == key: return avd return None def CreateAvd(avdName, abi): out = utils.RunCommand(["android", "create", "avd", "--name", avdName, "--target", TargetForAbi(abi), '--abi', abi], input="no\n") if out.find('Created AVD ') < 0: if VERBOSE: sys.stderr.write('Could not create AVD:\n%s\n' % out) raise utils.Error('Could not create AVD') def AvdExists(avdName): avdList = AndroidListAvd() return FindAvd(avdList, avdName) is not None def EnsureAvdExists(avdName, abi): if AvdExists(avdName): return if VERBOSE: sys.stderr.write('Checking SDK packages...\n') if EnsureAndroidSdkPackagesInstalled(abi): # Installing a new package could have made a previously invalid AVD valid if AvdExists(avdName): return CreateAvd(avdName, abi) def StartEmulator(abi, avdName, pollFn): """ Start an emulator for a given abi and svdName. Echo the emulator's stderr and stdout output to our stderr. Call pollFn repeatedly until it returns False. Leave the emulator running when we return. Implementation note: Normally we would call the 'emulator' binary, which is a wrapper that launches the appropriate abi-specific emulator. But there is a bug that causes the emulator to exit immediately with a result code of -11 if run from a ssh shell or a No Machine shell. (And only if called from three levels of nested python scripts.) Calling the ABI-specific versions of the emulator directly works around this bug. """ emulatorName = {'x86' : 'emulator-x86', 'armeabi-v7a': 'emulator-arm'}[abi] command = [emulatorName, '-avd', avdName, '-no-boot-anim', '-no-window'] utils.RunCommand(command, pollFn=pollFn, killOnEarlyReturn=False, outStream=sys.stderr, errStream=sys.stderr) def ParseAndroidDevices(text): """Return Dictionary [name] -> status""" text = text.split('List of devices attached')[-1] lines = [line.strip() for line in text.split('\n')] lines = [line for line in lines if len(line) > 0] devices = {} for line in lines: lineItems = line.split('\t') devices[lineItems[0]] = lineItems[1] return devices def GetAndroidDevices(): return ParseAndroidDevices(utils.RunCommand(["adb", "devices"])) def FilterOfflineDevices(devices): online = {} for device in devices.keys(): status = devices[device] if status != 'offline': online[device] = status return online def GetOnlineAndroidDevices(): return FilterOfflineDevices(GetAndroidDevices()) def GetAndroidDeviceProperty(device, property): return utils.RunCommand( ["adb", "-s", device, "shell", "getprop", property]).strip() def GetAndroidDeviceAbis(device): abis = [] for property in ['ro.product.cpu.abi', 'ro.product.cpu.abi2']: out = GetAndroidDeviceProperty(device, property) if len(out) > 0: abis.append(out) return abis def FindAndroidRunning(abi): for device in GetOnlineAndroidDevices().keys(): if abi in GetAndroidDeviceAbis(device): return device return None def AddSdkToolsToPath(): script_dir = os.path.dirname(sys.argv[0]) dart_root = os.path.realpath(os.path.join(script_dir, '..', '..')) third_party_root = os.path.join(dart_root, 'third_party') android_tools = os.path.join(third_party_root, 'android_tools') android_sdk_root = os.path.join(android_tools, 'sdk') android_sdk_tools = os.path.join(android_sdk_root, 'tools') android_sdk_platform_tools = os.path.join(android_sdk_root, 'platform-tools') os.environ['PATH'] = ':'.join([ os.environ['PATH'], android_sdk_tools, android_sdk_platform_tools]) # Remove any environment variables that would affect our build. for i in ['ANDROID_NDK_ROOT', 'ANDROID_SDK_ROOT', 'ANDROID_TOOLCHAIN', 'AR', 'BUILDTYPE', 'CC', 'CXX', 'GYP_DEFINES', 'LD_LIBRARY_PATH', 'LINK', 'MAKEFLAGS', 'MAKELEVEL', 'MAKEOVERRIDES', 'MFLAGS', 'NM']: if i in os.environ: del os.environ[i] def FindAndroid(abi, bootstrap): if VERBOSE: sys.stderr.write('Looking for an Android device running abi %s...\n' % abi) AddSdkToolsToPath() device = FindAndroidRunning(abi) if not device: if bootstrap: if VERBOSE: sys.stderr.write("No emulator found, try to create one.\n") avdName = 'dart-build-%s' % abi EnsureAvdExists(avdName, abi) # It takes a while to start up an emulator. # Provide feedback while we wait. pollResult = [None] def pollFunction(): if VERBOSE: sys.stderr.write('.') pollResult[0] = FindAndroidRunning(abi) # Stop polling once we have our result. return pollResult[0] != None StartEmulator(abi, avdName, pollFunction) device = pollResult[0] return device def Main(): # Parse options. parser = BuildOptions() (options, args) = parser.parse_args() if not ProcessOptions(options): parser.print_help() return 1 # If there are additional arguments, report error and exit. if args: parser.print_help() return 1 try: device = FindAndroid(options.abi, options.bootstrap) if device != None: sys.stdout.write("%s\n" % device) return 0 else: if VERBOSE: sys.stderr.write('Could not find device\n') return 2 except utils.Error as e: sys.stderr.write("error: %s\n" % e) if DEBUG: traceback.print_exc(file=sys.stderr) return -1 if __name__ == '__main__': sys.exit(Main())