PageRenderTime 56ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/buildutil/android.py

https://gitlab.com/adam.lukaitis/fplutil
Python | 1342 lines | 1267 code | 13 blank | 62 comment | 5 complexity | e1612350b4b88cb477a589bc7824620e MD5 | raw file
  1. # Copyright 2014 Google Inc. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. """@file buildutil/android.py Android-specific BuildEnvironment.
  16. @namespace buildutil.android
  17. Optional environment variables:
  18. @li ANT_PATH = Path to ant binary. Required if ant is not in $PATH,
  19. or not passed on command line.
  20. @li ANDROID_SDK_HOME = Path to the Android SDK. Required if it is not passed
  21. on the command line.
  22. @li NDK_HOME = Path to the Android NDK. Required if it is not in passed on the
  23. command line.
  24. """
  25. import datetime
  26. import errno
  27. import os
  28. import platform
  29. import random
  30. import re
  31. import shlex
  32. import shutil
  33. import stat
  34. import subprocess
  35. import sys
  36. import tempfile
  37. import time
  38. import uuid
  39. import xml.etree.ElementTree
  40. sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
  41. import buildutil.common as common
  42. _SDK_HOME_ENV_VAR = 'ANDROID_SDK_HOME'
  43. _NDK_HOME_ENV_VAR = 'NDK_HOME'
  44. _SDK_HOME = 'sdk_home'
  45. _NDK_HOME = 'ndk_home'
  46. _ANT_PATH_ENV_VAR = 'ANT_PATH'
  47. _ANT_PATH = 'ant_path'
  48. _ANT_FLAGS = 'ant_flags'
  49. _ANT_TARGET = 'ant_target'
  50. _APK_KEYSTORE = 'apk_keystore'
  51. _APK_PASSFILE = 'apk_passfile'
  52. _APK_KEYALIAS = 'apk_keyalias'
  53. _APK_KEYPK8 = 'apk_keypk8'
  54. _APK_KEYPEM = 'apk_keypem'
  55. _SIGN_APK = 'sign_apk'
  56. _MANIFEST_FILE = 'AndroidManifest.xml'
  57. _NDK_MAKEFILE = 'Android.mk'
  58. _ALWAYS_MAKE = 'always_make'
  59. _ADB_LOGCAT_ARGS = 'adb_logcat_args'
  60. _ADB_LOGCAT_MONITOR = 'adb_logcat_monitor'
  61. _IGNORE_SDK_VERSION_MISSING = 'ignore_sdk_version_missing'
  62. _MATCH_DEVICES = re.compile(r'^List of devices attached\s*')
  63. _MATCH_PACKAGE = re.compile(r'^package:(.*)')
  64. _GTEST_FAILED = re.compile(r'(\[\s*FAILEDs\*\]|.*FAILED TESTS)')
  65. _ACTION_MAIN = 'android.intent.action.MAIN'
  66. _CATEGORY_LAUNCHER = 'android.intent.category.LAUNCHER'
  67. _NATIVE_ACTIVITY = 'android.app.NativeActivity'
  68. _ANDROID_MANIFEST_SCHEMA = 'http://schemas.android.com/apk/res/android'
  69. class XMLFile(object):
  70. """XML file base class factored for testability.
  71. Subclasses implement process(self, etree) to process the parsed XML.
  72. On error, they should raise common.ConfigurationError.
  73. Attributes:
  74. path: Path to XML file as set in initializer.
  75. """
  76. def __init__(self, path):
  77. """Constructs the XMLFile for a specified path.
  78. Args:
  79. path: The absolute path to the manifest file.
  80. Raises:
  81. common.ConfigurationError: Manifest file missing.
  82. """
  83. if path and not os.path.exists(path):
  84. raise common.ConfigurationError(path, os.strerror(errno.ENOENT))
  85. self.path = path
  86. def parse(self):
  87. """Parse the XML file and extract useful information.
  88. Raises:
  89. ConfigurationError: Elements were missing or incorrect in the file.
  90. IOError: Could not open XML file.
  91. """
  92. with open(self.path, 'r') as xmlfile:
  93. self._parse(xmlfile)
  94. def _parse(self, xmlfile):
  95. try:
  96. etree = xml.etree.ElementTree.parse(xmlfile)
  97. self.process(etree)
  98. except xml.etree.ElementTree.ParseError as pe:
  99. raise common.ConfigurationError(self.path, 'XML parse error: ' + str(pe))
  100. class AndroidManifest(XMLFile):
  101. """Class that extracts build information from an AndroidManifest.xml.
  102. Attributes:
  103. min_sdk: Minimum SDK version from the uses-sdk element.
  104. target_sdk: Target SDK version from the uses-sdk element, or min_sdk if it
  105. is not set.
  106. package_name: Name of the package.
  107. activity_name: Name of the first activity in the manifest.
  108. activity_names: List of names of each of the activities.
  109. main_activity_name: Name of the main activity.
  110. lib_name: Name of the library loaded by android.app.NativeActivity.
  111. ignore_sdk_version_missing: If set, ignore issues when the manifest doesn't
  112. contain SDK versioning elements. This is intended to be used when the
  113. minSdkVersion is defined in gradle configuration.
  114. """
  115. class MissingActivityError(common.ConfigurationError):
  116. """Raised if an activity element isn't present in a manifest.
  117. Attribtues:
  118. manifest: Manifest instance which detected an error.
  119. """
  120. def __init__(self, path, error, manifest):
  121. """Initialize this MissingActivityError.
  122. Args:
  123. path: Path to file that generated the error.
  124. error: The specific error to report.
  125. manifest: Manifest instance which detected an error.
  126. """
  127. super(AndroidManifest.MissingActivityError, self).__init__(path, error)
  128. self.manifest = manifest
  129. def __init__(self, path, ignore_sdk_version_missing=False):
  130. """Constructs the AndroidManifest for a specified path.
  131. Args:
  132. path: The absolute path to the manifest file.
  133. ignore_sdk_version_missing: How to handle missing SDK version elements
  134. during manifest processing. If set to False, raise an exception. If
  135. set to True, processing will continue silently.
  136. Raises:
  137. ConfigurationError: Manifest file missing.
  138. """
  139. super(AndroidManifest, self).__init__(path)
  140. self.activity_name = ''
  141. self.activity_names = []
  142. self.lib_name = ''
  143. self.main_activity_name = ''
  144. self.min_sdk = 0
  145. self.package_name = ''
  146. self.target_sdk = 0
  147. self.ignore_sdk_version_missing = ignore_sdk_version_missing
  148. def _process_sdk_element(self, sdk_element):
  149. """Processes a "uses-sdk" XML element and stores min/target sdk attributes.
  150. Args:
  151. sdk_element: xml.etree.ElementTree that represents a "uses-sdk" clause
  152. in an AndroidManifest.
  153. Raises:
  154. common.ConfigurationError: sdk_element was None, or minSdkVersion missing
  155. """
  156. if sdk_element is None:
  157. raise common.ConfigurationError(self.path, 'uses-sdk element missing')
  158. min_sdk_version = AndroidManifest.__get_schema_attribute_value(
  159. sdk_element, 'minSdkVersion')
  160. if not min_sdk_version:
  161. raise common.ConfigurationError(self.path, 'minSdkVersion missing')
  162. target_sdk_version = AndroidManifest.__get_schema_attribute_value(
  163. sdk_element, 'targetSdkVersion')
  164. if not target_sdk_version:
  165. target_sdk_version = min_sdk_version
  166. self.min_sdk = int(min_sdk_version)
  167. self.target_sdk = int(target_sdk_version)
  168. def process(self, etree):
  169. """Process the parsed AndroidManifest to extract SDK version info.
  170. Args:
  171. etree: An xml.etree.ElementTree object of the parsed XML file.
  172. Raises:
  173. ConfigurationError: Required elements were missing or incorrect.
  174. MissingActivityError: If the main activity element isn't present. This
  175. instance will be completely populated when this exception is thrown.
  176. """
  177. root = etree.getroot()
  178. self.package_name = root.get('package')
  179. sdk_element = root.find('uses-sdk')
  180. # Attempt to process the element, but if ignore_sdk_version_missing is
  181. # False, ignore issues of sdk version elements not existing in the
  182. # manifest.
  183. try:
  184. self._process_sdk_element(sdk_element)
  185. except common.ConfigurationError as e:
  186. if not self.ignore_sdk_version_missing:
  187. raise e
  188. app_element = root.find('application')
  189. if app_element is None:
  190. raise common.ConfigurationError(self.path, 'application missing')
  191. if not self.package_name:
  192. raise common.ConfigurationError(self.path, 'package missing')
  193. activity_elements = app_element.findall('activity')
  194. for activity_element in activity_elements:
  195. activity_name = AndroidManifest.__get_schema_attribute_value(
  196. activity_element, 'name')
  197. self.activity_names.append(activity_name)
  198. if AndroidManifest._check_main_activity(activity_element):
  199. main_activity = activity_element
  200. self.main_activity_name = activity_name
  201. if self.main_activity_name == _NATIVE_ACTIVITY:
  202. for metadata_element in main_activity.findall('meta-data'):
  203. if (AndroidManifest.__get_schema_attribute_value(
  204. metadata_element, 'name') == 'android.app.lib_name'):
  205. self.lib_name = AndroidManifest.__get_schema_attribute_value(
  206. metadata_element, 'value')
  207. if not self.lib_name:
  208. raise common.ConfigurationError(
  209. self.path, 'meta-data android.app.lib_name missing')
  210. elif not self.main_activity_name:
  211. raise AndroidManifest.MissingActivityError(
  212. self.path, 'main activity missing', self)
  213. # For backwards compatability.
  214. if self.activity_names:
  215. self.activity_name = self.activity_names[0]
  216. @staticmethod
  217. def _check_main_activity(activity_xml_element):
  218. """Helper function to determine if an activity is considered main.
  219. An activity is considered main if it has an intent-filter, filters for
  220. the main action, and the category launcher.
  221. Args:
  222. activity_xml_element: xml.etree.ElementTree of an Android activity.
  223. Returns:
  224. True if the activity is considered main, False otherwise.
  225. """
  226. intent_filter_element = activity_xml_element.find('intent-filter')
  227. if intent_filter_element is None:
  228. return False
  229. action_elements = intent_filter_element.findall('action')
  230. action_names = (AndroidManifest.__get_schema_attribute_value(a, 'name')
  231. for a in action_elements)
  232. if _ACTION_MAIN not in action_names:
  233. return False
  234. category_elements = intent_filter_element.findall('category')
  235. category_names = (AndroidManifest.__get_schema_attribute_value(c, 'name')
  236. for c in category_elements)
  237. if _CATEGORY_LAUNCHER not in category_names:
  238. return False
  239. return True
  240. @staticmethod
  241. def __get_schema_attribute_value(xml_element, attribute):
  242. """Get attribute from xml_element using the Android manifest schema.
  243. Args:
  244. xml_element: xml.etree.ElementTree to query.
  245. attribute: Name of Android Manifest attribute to retrieve.
  246. Returns:
  247. XML attribute string from the specified element.
  248. """
  249. return xml_element.get('{%s}%s' % (_ANDROID_MANIFEST_SCHEMA, attribute))
  250. class BuildXml(XMLFile):
  251. """Class that extracts build information from an ant build.xml.
  252. Attributes:
  253. project_name: The name of the project, used by ant to name output files.
  254. """
  255. def __init__(self, path):
  256. """Constructs the BuildXml for a specified path.
  257. Args:
  258. path: The absolute path to the build.xml file.
  259. Raises:
  260. ConfigurationError: build.xml file missing.
  261. """
  262. super(BuildXml, self).__init__(path)
  263. self.project_name = None
  264. def process(self, etree):
  265. """Process the parsed build.xml to extract project info.
  266. Args:
  267. etree: An xml.etree.ElementTree object of the parsed XML file.
  268. Raises:
  269. ConfigurationError: Required elements were missing or incorrect.
  270. """
  271. project_element = etree.getroot()
  272. if project_element.tag != 'project':
  273. raise common.ConfigurationError(self.path, 'project element missing')
  274. self.project_name = project_element.get('name')
  275. if not self.project_name:
  276. raise common.ConfigurationError(self.path, 'project name missing')
  277. class AdbDevice(object):
  278. """Stores information about an Android device.
  279. Attributes:
  280. serial: Serial number of the device.
  281. type: Type of device.
  282. usb: USB location.
  283. product: Product codename.
  284. device: Device codename.
  285. model: Model name.
  286. """
  287. def __init__(self, adb_device_line=None):
  288. """Initialize this instance from a device line from "adb devices -l".
  289. Args:
  290. adb_device_line: Device line from "adb devices -l".
  291. """
  292. self.serial = ''
  293. self.type = ''
  294. self.usb = ''
  295. self.product = ''
  296. self.device = ''
  297. self.model = ''
  298. # 'adb device -l' returns a line per device according to this format:
  299. # SERIAL TYPE [KEY:VALUE ...]
  300. if adb_device_line:
  301. tokens = adb_device_line.split()
  302. if len(tokens) < 2:
  303. print >>sys.stderr, ('Warning: AdbDevice initialized with '
  304. 'adb_device_line "{}", expected "SERIAL TYPE '
  305. '[KEY:VALUE ...]"'.format(adb_device_line))
  306. else:
  307. self.serial = tokens[0]
  308. self.type = tokens[1]
  309. for token in tokens[2:]:
  310. key_value = token.split(':')
  311. if len(key_value) != 2:
  312. continue
  313. setattr(self, key_value[0], key_value[1])
  314. def __str__(self):
  315. """Convert this instance into a string representation.
  316. Returns:
  317. A string in the form "key0:value0 key1:value1 ... keyN:valueN"
  318. where key is an attribute of this instance and value is the value of
  319. the attribute.
  320. """
  321. return ' '.join([':'.join([k, v]) for k, v in
  322. sorted(self.__dict__.iteritems())])
  323. class BuildEnvironment(common.BuildEnvironment):
  324. """Class representing an Android build environment.
  325. This class adds Android-specific functionality to the common
  326. BuildEnvironment.
  327. Attributes:
  328. ndk_home: Path to the Android NDK, if found.
  329. sdk_home: Path to the Android SDK, if found.
  330. ant_path: Path to the ant binary, if found.
  331. ant_flags: Flags to pass to the ant binary, if used.
  332. ant_target: Ant build target name.
  333. sign_apk: Enable signing of Android APKs.
  334. apk_keystore: Keystore file path to use when signing an APK.
  335. apk_keyalias: Alias of key to use when signing an APK.
  336. apk_passfile: Path to file containing a password to use when signing an
  337. APK.
  338. apk_keycertpair: (key, cert) tuple where Key is a .pk8 key file and
  339. cert is a .pem certificate file.
  340. adb_logcat_args: List of additional arguments passed to logcat when
  341. monitoring the application's output.
  342. adb_logcat_monitor: Whether to continue to monitor the application's
  343. output after it has launched or has been destroyed.
  344. always_make: Whether to build when the project is already up to date.
  345. """
  346. ADB = 'adb'
  347. ANDROID = 'android'
  348. ANT = 'ant'
  349. JARSIGNER = 'jarsigner'
  350. KEYTOOL = 'keytool'
  351. NDK_BUILD = 'ndk-build'
  352. ZIPALIGN = 'zipalign'
  353. def __init__(self, arguments):
  354. """Constructs the BuildEnvironment with basic information needed to build.
  355. The build properties as set by argument parsing are also available
  356. to be modified by code using this object after construction.
  357. It is required to call this function with a valid arguments object,
  358. obtained either by calling argparse.ArgumentParser.parse_args() after
  359. adding this modules arguments via BuildEnvironment.add_arguments(), or
  360. by passing in an object returned from BuildEnvironment.build_defaults().
  361. Args:
  362. arguments: The argument object returned from ArgumentParser.parse_args().
  363. """
  364. super(BuildEnvironment, self).__init__(arguments)
  365. if type(arguments) is dict:
  366. args = arguments
  367. else:
  368. args = vars(arguments)
  369. self.ndk_home = args[_NDK_HOME]
  370. self.sdk_home = args[_SDK_HOME]
  371. self.ant_path = args[_ANT_PATH]
  372. self.ant_flags = args[_ANT_FLAGS]
  373. self.ant_target = args[_ANT_TARGET]
  374. self.sign_apk = args[_SIGN_APK]
  375. self.apk_keystore = args[_APK_KEYSTORE]
  376. self.apk_keyalias = args[_APK_KEYALIAS]
  377. self.apk_passfile = args[_APK_PASSFILE]
  378. self.apk_keycertpair = None
  379. if args[_APK_KEYPK8] and args[_APK_KEYPEM]:
  380. self.apk_keycertpair = (args[_APK_KEYPK8], args[_APK_KEYPEM])
  381. self.always_make = args[_ALWAYS_MAKE]
  382. self.adb_logcat_args = args[_ADB_LOGCAT_ARGS]
  383. self.adb_logcat_monitor = args[_ADB_LOGCAT_MONITOR]
  384. self.ignore_sdk_version_missing = args[_IGNORE_SDK_VERSION_MISSING]
  385. @staticmethod
  386. def build_defaults():
  387. """Helper function to set build defaults.
  388. Returns:
  389. A dict containing appropriate defaults for a build.
  390. """
  391. args = common.BuildEnvironment.build_defaults()
  392. args[_SDK_HOME] = (os.getenv(_SDK_HOME_ENV_VAR) or
  393. common.BuildEnvironment._find_path_from_binary(
  394. BuildEnvironment.ANDROID, 2))
  395. args[_NDK_HOME] = (os.getenv(_NDK_HOME_ENV_VAR) or
  396. common.BuildEnvironment._find_path_from_binary(
  397. BuildEnvironment.NDK_BUILD, 1))
  398. args[_ANT_PATH] = (
  399. os.getenv(_ANT_PATH_ENV_VAR) or
  400. common._find_executable(BuildEnvironment.ANT))
  401. args[_ANT_FLAGS] = '-quiet'
  402. args[_ANT_TARGET] = 'release'
  403. args[_APK_KEYSTORE] = None
  404. args[_APK_KEYALIAS] = None
  405. args[_APK_PASSFILE] = None
  406. args[_APK_KEYPK8] = None
  407. args[_APK_KEYPEM] = None
  408. args[_SIGN_APK] = False
  409. args[_ALWAYS_MAKE] = False
  410. args[_ADB_LOGCAT_ARGS] = []
  411. args[_ADB_LOGCAT_MONITOR] = False
  412. # We expect an SDK version check by default.
  413. args[_IGNORE_SDK_VERSION_MISSING] = False
  414. return args
  415. @staticmethod
  416. def add_arguments(parser):
  417. """Add module-specific command line arguments to an argparse parser.
  418. This will take an argument parser and add arguments appropriate for this
  419. module. It will also set appropriate default values.
  420. Args:
  421. parser: The argparse.ArgumentParser instance to use.
  422. """
  423. defaults = BuildEnvironment.build_defaults()
  424. common.BuildEnvironment.add_arguments(parser)
  425. parser.add_argument('-n', '--' + _NDK_HOME,
  426. help='Path to Android NDK', dest=_NDK_HOME,
  427. default=defaults[_NDK_HOME])
  428. parser.add_argument('-s', '--' + _SDK_HOME,
  429. help='Path to Android SDK', dest=_SDK_HOME,
  430. default=defaults[_SDK_HOME])
  431. parser.add_argument('-a', '--' + _ANT_PATH,
  432. help='Path to ant binary', dest=_ANT_PATH,
  433. default=defaults[_ANT_PATH])
  434. parser.add_argument('-A', '--' + _ANT_FLAGS,
  435. help='Flags to use to override ant flags',
  436. dest=_ANT_FLAGS, default=defaults[_ANT_FLAGS])
  437. parser.add_argument('-T', '--' + _ANT_TARGET,
  438. help=('Target to use for ant build. If the clean '
  439. 'option is specified, this is ignored.'),
  440. dest=_ANT_TARGET, default=defaults[_ANT_TARGET])
  441. parser.add_argument('-k', '--' + _APK_KEYSTORE,
  442. help='Path to keystore to use when signing an APK',
  443. dest=_APK_KEYSTORE, default=defaults[_APK_KEYSTORE])
  444. parser.add_argument('-K', '--' + _APK_KEYALIAS,
  445. help='Key alias to use when signing an APK',
  446. dest=_APK_KEYALIAS, default=defaults[_APK_KEYALIAS])
  447. parser.add_argument('-P', '--' + _APK_PASSFILE,
  448. help='Path to file containing keystore password',
  449. dest=_APK_PASSFILE, default=defaults[_APK_PASSFILE])
  450. parser.add_argument('--' + _APK_KEYPK8,
  451. help=('.pk8 key file used to sign an APK. To use '
  452. 'this option %s must also specified a '
  453. 'certificate file.' % _APK_KEYPEM),
  454. dest=_APK_KEYPK8, default=defaults[_APK_KEYPK8])
  455. parser.add_argument('--' + _APK_KEYPEM,
  456. help=('.pem certificate file used to sign an APK. '
  457. 'To use this option %s must also specified a '
  458. 'key file.' % _APK_KEYPK8),
  459. dest=_APK_KEYPEM, default=defaults[_APK_KEYPEM])
  460. parser.add_argument('-S', '--' + _SIGN_APK,
  461. help='Enable signing of Android APKs.',
  462. dest=_SIGN_APK, action='store_true',
  463. default=defaults[_SIGN_APK])
  464. parser.add_argument('-B', '--' + _ALWAYS_MAKE,
  465. help='Always build all up to date targets.',
  466. dest=_ALWAYS_MAKE, action='store_true')
  467. parser.add_argument('--no-' + _ALWAYS_MAKE,
  468. help='Only build out of date targets.',
  469. dest=_ALWAYS_MAKE, action='store_false')
  470. parser.add_argument('--' + _ADB_LOGCAT_ARGS,
  471. help='Additonal arguments to pass to logcat',
  472. dest=_ADB_LOGCAT_ARGS,
  473. default=defaults[_ADB_LOGCAT_ARGS], nargs='+')
  474. parser.add_argument('--' + _ADB_LOGCAT_MONITOR,
  475. help=('Whether to continue to monitor logcat output '
  476. 'after the application has been displayed or '
  477. 'destroyed.'),
  478. dest=_ADB_LOGCAT_MONITOR, action='store_true',
  479. default=defaults[_ADB_LOGCAT_MONITOR])
  480. parser.add_argument('--' + _IGNORE_SDK_VERSION_MISSING,
  481. help=('Disable SDK version checks when reading the '
  482. 'application manifest. This is required in '
  483. 'cases where SDK versions are specified in '
  484. 'gradle project files instead of the manifest.'),
  485. action='store_true',
  486. default=defaults[_IGNORE_SDK_VERSION_MISSING])
  487. parser.set_defaults(
  488. **{_ALWAYS_MAKE: defaults[_ALWAYS_MAKE]}) # pylint: disable=star-args
  489. def _find_binary(self, binary, additional_paths=None):
  490. """Find a binary from the set of binaries managed by this class.
  491. This method enables the lookup of a binary path using the name of the
  492. binary to avoid replication of code which searches for binaries.
  493. This class allows the lookup of...
  494. * BuildEnvironment.ADB
  495. * BuildEnvironment.ANDROID
  496. * BuildEnvironment.ANT
  497. * BuildEnvironment.JARSIGNER
  498. * BuildEnvironment.KEYTOOL
  499. * BuildEnvironment.NDK_BUILD
  500. * BuildEnvironment.ZIPALIGN
  501. The _find_binary() method in derived classes may add more binaries.
  502. Args:
  503. binary: Name of the binary.
  504. additional_paths: Additional dictionary to search for binary paths.
  505. Returns:
  506. String containing the path of binary.
  507. Raises:
  508. ToolPathError: Binary is not at the specified path.
  509. """
  510. ndk_build_paths = []
  511. if self.ndk_home:
  512. ndk_build_paths = [os.path.join(self.ndk_home, '')]
  513. # zipalign is under the sdk/build-tools subdirectory in ADT 20140702
  514. # or newer. In older ADT releases zipalign was located in sdk/tools.
  515. zip_align_paths = []
  516. if binary == BuildEnvironment.ZIPALIGN:
  517. zip_align_paths = [os.path.join(self.sdk_home, 'tools', '')]
  518. for root, dirs, _ in os.walk(os.path.join(self.sdk_home, 'build-tools')):
  519. zip_align_paths.extend([os.path.join(root, d, '') for d in dirs])
  520. break
  521. if not self.sdk_home and not ndk_build_paths:
  522. raise common.ToolPathError('Android SDK and NDK', '[unknown]')
  523. elif not ndk_build_paths:
  524. raise common.ToolPathError('Android NDK', '[unknown]')
  525. elif not self.sdk_home:
  526. raise common.ToolPathError('Android SDK', '[unknown]')
  527. search_dict = {
  528. BuildEnvironment.ADB: [os.path.join(
  529. self.sdk_home, 'platform-tools', '')],
  530. BuildEnvironment.ANDROID: [
  531. os.path.join(self.sdk_home, 'tools', '')],
  532. BuildEnvironment.ANT: [self.ant_path],
  533. BuildEnvironment.NDK_BUILD: ndk_build_paths,
  534. BuildEnvironment.JARSIGNER: [],
  535. BuildEnvironment.KEYTOOL: [],
  536. BuildEnvironment.ZIPALIGN: zip_align_paths}
  537. if additional_paths:
  538. search_dict.append(additional_paths)
  539. return common.BuildEnvironment._find_binary(self, binary, search_dict)
  540. def build_android_libraries(self, subprojects, output=None):
  541. """Build list of Android library projects.
  542. This function iteratively runs ndk-build over a list of paths relative
  543. to the current project directory.
  544. Args:
  545. subprojects: A list pf paths relative to the project directory to build.
  546. output: An optional directory relative to the project directory to
  547. receive the build output.
  548. Raises:
  549. SubCommandError: ndk-build invocation failed or returned an error.
  550. ToolPathError: Android NDK location not found in configured build
  551. environment or $PATH.
  552. """
  553. ndk_build = self._find_binary(BuildEnvironment.NDK_BUILD)
  554. for p in subprojects:
  555. # Disable parallel clean on OSX.
  556. cpu_count = self.cpu_count
  557. if self.clean and platform.mac_ver()[0]:
  558. cpu_count = 1
  559. args = [ndk_build, '-j' + str(cpu_count)]
  560. if self.always_make:
  561. args.append('-B')
  562. args += ['-C', self.get_project_directory(path=p)]
  563. if self.clean:
  564. args.append('clean')
  565. if self.verbose:
  566. args.append('V=1')
  567. if output:
  568. args.append(
  569. 'NDK_OUT=%s' % self.get_project_directory(path=output))
  570. if self.make_flags:
  571. args += shlex.split(self.make_flags, posix=self._posix)
  572. self.run_subprocess(args)
  573. def _find_best_android_sdk(self, android, minsdk, target):
  574. """Finds the best installed Android SDK for a project.
  575. Based on the passed in min and target SDK levels, find the highest SDK
  576. level installed that is greater than the specified minsdk, up to the
  577. target sdk level. Return it as an API level string.
  578. Otherwise, if the minimum installed SDK is greater than the
  579. targetSdkVersion, return the maximum installed SDK version, or raise a
  580. ConfigurationError if no installed SDK meets the min SDK.
  581. Args:
  582. android: Path to android tool binary.
  583. minsdk: Integer minimum SDK level.
  584. target: Integer target SDK level.
  585. Returns:
  586. Highest installed Android SDK API level in the range, as a string.
  587. Raises:
  588. SubCommandError: NDK toolchain invocation failed or returned an error.
  589. ToolPathError: Android NDK or SDK location not found in configured build
  590. environment or $PATH, or ant not found.
  591. ConfigurationError: Required build configuration file missing or broken
  592. in an unrecoverable way.
  593. """
  594. acmd = [android, 'list', 'target', '--compact']
  595. (stdout, unused_stderr) = self.run_subprocess(acmd, capture=True)
  596. if self.verbose:
  597. print 'android list target returned: {%s}' % (stdout)
  598. # Find the highest installed SDK <= targetSdkVersion, if possible.
  599. #
  600. # 'android list target --compact' will output lines like:
  601. #
  602. # android-1
  603. # android-2
  604. #
  605. # for installed SDK targets, along with other info not starting with
  606. # android-.
  607. installed = 0
  608. for line in stdout.splitlines():
  609. l = line.strip()
  610. if l.startswith('android-'):
  611. nstr = l.split('-')[1]
  612. # Ignore preview SDK revisions (e.g "L").
  613. if not nstr.isdigit():
  614. continue
  615. n = int(nstr)
  616. if n > installed:
  617. if self.verbose:
  618. print 'sdk api level %d found' % (n)
  619. installed = n
  620. if installed == target:
  621. break
  622. if installed < minsdk:
  623. raise common.ConfigurationError(self.sdk_home,
  624. ('Project requires Android SDK %d, '
  625. 'but only found up to %d' %
  626. (minsdk, installed)))
  627. apitarget = 'android-%d' % (installed)
  628. return apitarget
  629. def get_manifest_path(self, path='.'):
  630. """Get the path of the manifest file.
  631. Args:
  632. path: Optional relative path from project directory to project to build.
  633. Returns:
  634. Path of the manifest file.
  635. """
  636. return os.path.join(self.get_project_directory(path=path), _MANIFEST_FILE)
  637. def parse_manifest(self, path='.'):
  638. """Parse the project's manifest.
  639. Args:
  640. path: Optional relative path from project directory to project to build.
  641. Returns:
  642. AndroidManifest instance parsed from the project manifest.
  643. Raises:
  644. ConfigurationError: Required elements were missing or incorrect.
  645. MissingActivityError: If a main activity element isn't present.
  646. """
  647. manifest = AndroidManifest(self.get_manifest_path(path=path),
  648. ignore_sdk_version_missing=
  649. self.ignore_sdk_version_missing)
  650. manifest.parse()
  651. return manifest
  652. def create_update_build_xml(self, manifest, path='.'):
  653. """Create or update ant build.xml for an Android project.
  654. Args:
  655. manifest: Parsed AndroidManifest instance.
  656. path: Optional relative path from project directory to project to build.
  657. Returns:
  658. BuildXml instance which references the created / updated ant project.
  659. """
  660. android = self._find_binary(BuildEnvironment.ANDROID)
  661. project = self.get_project_directory(path=path)
  662. buildxml_path = os.path.join(project, 'build.xml')
  663. # Get the last component of the package name for the application name.
  664. app_name = manifest.package_name[manifest.package_name.rfind('.') + 1:]
  665. # If no build.xml exists, create one for the project in the directory
  666. # we are currently building.
  667. if (not os.path.exists(buildxml_path) or
  668. os.path.getmtime(buildxml_path) < os.path.getmtime(manifest.path)):
  669. apitarget = self._find_best_android_sdk(android, manifest.min_sdk,
  670. manifest.target_sdk)
  671. self.run_subprocess([android, 'update', 'project', '--path', project,
  672. '--target', apitarget, '--name', app_name])
  673. buildxml = BuildXml(buildxml_path)
  674. buildxml.parse()
  675. return buildxml
  676. def get_apk_filenames(self, app_name, path='.'):
  677. """Get the set of output APK names for the project.
  678. Args:
  679. app_name: Basename of the APK parsed from build.xml.
  680. path: Relative path from project directory to project to build.
  681. Returns:
  682. (signed_apkpath, unsigned_apkpath) where signed_apkpath and
  683. unsigned_apkpath are paths to the signed and unsigned APKs respectively.
  684. Signing is optional so the signed APK may not be present when the
  685. project has been built successfully.
  686. """
  687. # ant outputs to $PWD/bin. The APK will have a name as constructed below.
  688. project_directory = self.get_project_directory(path=path)
  689. apk_directory = os.path.join(project_directory, 'bin')
  690. if self.ant_target == 'debug':
  691. unsigned_apkpath = os.path.join(apk_directory, '%s-%s.apk' % (
  692. app_name, self.ant_target))
  693. signed_apkpath = unsigned_apkpath
  694. else:
  695. unsigned_apkpath = os.path.join(apk_directory, '%s-%s-unsigned.apk' % (
  696. app_name, self.ant_target))
  697. signed_apkpath = os.path.join(apk_directory, '%s.apk' % app_name)
  698. return (signed_apkpath, unsigned_apkpath)
  699. def build_android_apk(self, path='.', output=None, manifest=None):
  700. """Build an Android APK.
  701. This function builds an APK by using ndk-build and ant, at an optionally
  702. specified relative path from the current project directory, and output to
  703. an optionally specified output directory, also relative to the current
  704. project directory. Flags are passed to ndk-build and ant as specified in
  705. the build environment. This function does not install the resulting APK.
  706. If no build.xml is found, one is generated via the 'android' command, if
  707. possible.
  708. Args:
  709. path: Optional relative path from project directory to project to build.
  710. output: Optional relative path from project directory to output
  711. directory.
  712. manifest: Parsed AndroidManifest instance.
  713. Raises:
  714. SubCommandError: NDK toolchain invocation failed or returned an error.
  715. ToolPathError: Android NDK or SDK location not found in configured build
  716. environment or $PATH, or ant not found.
  717. ConfigurationError: Required build configuration file missing or broken
  718. in an unrecoverable way.
  719. IOError: An error occurred writing or copying the APK.
  720. """
  721. ant = self._find_binary(BuildEnvironment.ANT)
  722. build_apk = True
  723. try:
  724. if not manifest:
  725. manifest = self.parse_manifest(path=path)
  726. except AndroidManifest.MissingActivityError as e:
  727. # If the activity is missing it's still possible to build the project.
  728. print >> sys.stderr, str(e)
  729. manifest = e.manifest
  730. build_apk = False
  731. # Create or update build.xml for ant.
  732. buildxml = self.create_update_build_xml(
  733. manifest if manifest else self.parse_manifest(path=path),
  734. path=path)
  735. acmd = [ant, 'clean' if self.clean else self.ant_target]
  736. if self.ant_flags:
  737. acmd += shlex.split(self.ant_flags, posix=self._posix)
  738. self.run_subprocess(acmd, cwd=path)
  739. if not build_apk:
  740. return
  741. signed_apkpath, unsigned_apkpath = self.get_apk_filenames(
  742. buildxml.project_name, path=path)
  743. source_apkpath = unsigned_apkpath
  744. if self.sign_apk and not self.clean:
  745. if self.ant_target != 'debug':
  746. source_apkpath = signed_apkpath
  747. self._sign_apk(unsigned_apkpath, signed_apkpath)
  748. else:
  749. print >>sys.stderr, 'Signing not required for debug target %s.' % (
  750. unsigned_apkpath)
  751. if output and not self.clean:
  752. out_abs = self.get_project_directory(path=output)
  753. if not os.path.exists(out_abs):
  754. os.makedirs(out_abs)
  755. if self.verbose:
  756. print 'Copying apk %s to: %s' % (source_apkpath, out_abs)
  757. shutil.copy2(source_apkpath, out_abs)
  758. @staticmethod
  759. def generate_password():
  760. """Generate a psuedo random password.
  761. Returns:
  762. 8 character hexadecimal string.
  763. """
  764. return '%08x' % (random.random() * 16 ** 8)
  765. def _sign_apk(self, source, target):
  766. """This function signs an Android APK, optionally generating a key.
  767. This function signs an APK using a keystore and password as configured
  768. in the build configuration. If none are configured, it generates an
  769. ephemeral key good for 60 days.
  770. Args:
  771. source: Absolute path to source APK to sign.
  772. target: Target path to write signed APK to.
  773. Raises:
  774. SubCommandError: Jarsigner invocation failed or returned an error.
  775. ToolPathError: Jarsigner or keygen location not found in $PATH.
  776. ConfigurationError: User specified some but not all signing parameters.
  777. IOError: An error occurred copying the APK.
  778. """
  779. # Debug targets are automatically signed and aligned by ant.
  780. if self.ant_target is 'debug':
  781. return
  782. keystore = self.apk_keystore
  783. passfile = self.apk_passfile
  784. alias = self.apk_keyalias
  785. # If any of keystore, passwdfile, or alias are None we will create a
  786. # temporary keystore with a random password and alias and remove it after
  787. # signing. This facilitates testing release builds when the release
  788. # keystore is not available (such as in a continuous testing environment).
  789. ephemeral = False
  790. # Exit and don't sign if the source file is older than the target.
  791. if os.path.exists(target):
  792. if os.path.getmtime(source) < os.path.getmtime(target):
  793. return
  794. # If a key / cert pair is specified, generate a temporary key store to sign
  795. # the APK.
  796. temp_directory = ''
  797. if self.apk_keycertpair:
  798. key = tempfile.NamedTemporaryFile()
  799. self.run_subprocess(('openssl', 'pkcs8', '-inform', 'DER', '-nocrypt',
  800. '-in', self.apk_keycertpair[0], '-out', key.name))
  801. p12 = tempfile.NamedTemporaryFile()
  802. password_file = tempfile.NamedTemporaryFile()
  803. password = BuildEnvironment.generate_password()
  804. passfile = password_file.name
  805. password_file.write(password)
  806. password_file.flush()
  807. alias = BuildEnvironment.generate_password()
  808. self.run_subprocess(('openssl', 'pkcs12', '-export', '-in',
  809. self.apk_keycertpair[1], '-inkey', key.name,
  810. '-out', p12.name, '-password', 'pass:' + password,
  811. '-name', alias))
  812. key.close()
  813. temp_directory = tempfile.mkdtemp()
  814. keystore = os.path.join(temp_directory, 'temp.keystore')
  815. self.run_subprocess(('keytool', '-importkeystore', '-deststorepass',
  816. password, '-destkeystore', keystore,
  817. '-srckeystore', p12.name, '-srcstoretype', 'PKCS12',
  818. '-srcstorepass', password))
  819. p12.close()
  820. try:
  821. if not keystore or not passfile or not alias:
  822. # If the user specifies any of these, they need to specify them all,
  823. # otherwise we may overwrite one of them.
  824. if keystore:
  825. raise common.ConfigurationError(keystore,
  826. ('Must specify all of keystore, '
  827. 'password file, and alias'))
  828. if passfile:
  829. raise common.ConfigurationError(passfile,
  830. ('Must specify all of keystore, '
  831. 'password file, and alias'))
  832. if alias:
  833. raise common.ConfigurationError(alias,
  834. ('Must specify all of keystore, '
  835. 'password file, and alias'))
  836. ephemeral = True
  837. keystore = source + '.keystore'
  838. passfile = source + '.password'
  839. if self.verbose:
  840. print ('Creating ephemeral keystore file %s and password file %s' %
  841. (keystore, passfile))
  842. password = BuildEnvironment.generate_password()
  843. with open(passfile, 'w') as pf:
  844. if self._posix:
  845. os.fchmod(pf.fileno(), stat.S_IRUSR | stat.S_IWUSR)
  846. pf.write(password)
  847. alias = os.path.basename(source).split('.')[0]
  848. # NOTE: The password is passed via the command line for compatibility
  849. # with JDK 6. Move to use -storepass:file and -keypass:file when
  850. # JDK 7 is a requirement for Android development.
  851. acmd = [self._find_binary(BuildEnvironment.KEYTOOL), '-genkeypair',
  852. '-v', '-dname', 'cn=, ou=%s, o=fpl' % alias, '-storepass',
  853. password, '-keypass', password, '-keystore', keystore,
  854. '-alias', alias, '-keyalg', 'RSA', '-keysize', '2048',
  855. '-validity', '60']
  856. self.run_subprocess(acmd)
  857. tmpapk = target + '.tmp'
  858. if self.verbose:
  859. print 'Copying APK %s for signing as %s' % (source, tmpapk)
  860. shutil.copy2(source, tmpapk)
  861. with open(passfile, 'r') as pf:
  862. password = pf.read()
  863. # NOTE: The password is passed via stdin for compatibility with JDK 6
  864. # which - unlike the use of keytool above - ensures the password is
  865. # not visible when displaying the command lines of processes of *nix
  866. # operating systems like Linux and OSX.
  867. # Move to use -storepass:file and -keypass:file when JDK 7 is a
  868. # requirement for Android development.
  869. password_stdin = os.linesep.join(
  870. [password, password, # Store password and confirmation.
  871. password, password]) # Key password and confirmation.
  872. acmd = [self._find_binary(BuildEnvironment.JARSIGNER),
  873. '-verbose', '-sigalg', 'SHA1withRSA', '-digestalg',
  874. 'SHA1', '-keystore', keystore, tmpapk, alias]
  875. self.run_subprocess(acmd, stdin=password_stdin)
  876. # We want to align the APK for more efficient access on the device.
  877. # See:
  878. # http://developer.android.com/tools/help/zipalign.html
  879. acmd = [self._find_binary(BuildEnvironment.ZIPALIGN), '-f']
  880. if self.verbose:
  881. acmd.append('-v')
  882. acmd += ['4', tmpapk, target] # alignment == 4
  883. self.run_subprocess(acmd)
  884. finally:
  885. if temp_directory and os.path.exists(temp_directory):
  886. shutil.rmtree(temp_directory)
  887. if ephemeral:
  888. if self.verbose:
  889. print 'Removing ephemeral keystore and password files'
  890. if os.path.exists(keystore):
  891. os.unlink(keystore)
  892. if os.path.exists(passfile):
  893. os.unlink(passfile)
  894. def find_projects(self, path='.', exclude_dirs=None):
  895. """Find all Android projects under the specified path.
  896. Args:
  897. path: Path to start the search in, defaults to '.'.
  898. exclude_dirs: List of directory names to exclude from project
  899. detection in addition to ['bin', 'obj', 'res'], which are always
  900. excluded.
  901. Returns:
  902. (apk_dirs, lib_dirs) where apk_dirs is the list of directories which
  903. contain Android projects that build an APK and lib_dirs is alist of
  904. Android project directories that only build native libraries.
  905. """
  906. project = self.get_project_directory(path=path)
  907. apk_dir_set = set()
  908. module_dir_set = set()
  909. # Exclude paths where buildutil or ndk-build may generate or copy files.
  910. exclude = (exclude_dirs if exclude_dirs else []) + ['bin', 'obj', 'res']
  911. if type(exclude_dirs) is list:
  912. exclude += exclude_dirs
  913. for root, dirs, files in os.walk(project, followlinks=True):
  914. for ex in exclude:
  915. if ex in dirs:
  916. dirs.remove(ex)
  917. if _MANIFEST_FILE in files:
  918. apk_dir_set.add(root)
  919. if _NDK_MAKEFILE in files:
  920. p = root
  921. # Handle the use or nonuse of the jni subdir.
  922. if os.path.basename(p) == 'jni':
  923. p = os.path.dirname(p)
  924. module_dir_set.add(p)
  925. return (list(apk_dir_set), list(module_dir_set))
  926. def build_all(self, path='.', apk_output='apks', lib_output='libs',
  927. exclude_dirs=None):
  928. """Locate and build all Android sub-projects as appropriate.
  929. This function will recursively scan a directory tree for Android library
  930. and application projects and build them with the current build defaults.
  931. This will not work for projects which only wish for subsets to be built
  932. or have complicated external manipulation of makefiles and manifests, but
  933. it should handle the majority of projects as a reasonable default build.
  934. Args:
  935. path: Optional path to start the search in, defaults to '.'.
  936. apk_output: Optional path to apk output directory, default is 'apks'.
  937. lib_output: Optional path to library output directory, default is 'libs'.
  938. exclude_dirs: Optional list of directory names to exclude from project
  939. detection in addition to
  940. [apk_output, lib_output, 'bin', 'obj', 'res'],
  941. which are always excluded.
  942. Returns:
  943. (retval, errmsg) tuple of an integer return value suitable for returning
  944. to the invoking shell, and an error string (if any) or None (on success).
  945. """
  946. retval = 0
  947. errmsg = None
  948. apk_dirs, lib_dirs = self.find_projects(
  949. path=path, exclude_dirs=([apk_output, lib_output] + (
  950. exclude_dirs if exclude_dirs else [])))
  951. if self.verbose:
  952. print 'Found APK projects in: %s' % str(apk_dirs)
  953. print 'Found library projects in: %s' % str(lib_dirs)
  954. try:
  955. self.build_android_libraries(lib_dirs, output=lib_output)
  956. for apk in apk_dirs:
  957. self.build_android_apk(path=apk, output=apk_output)
  958. retval = 0
  959. except common.Error as e:
  960. errmsg = 'Caught buildutil error: %s' % e.error_message
  961. retval = e.error_code
  962. except IOError as e:
  963. errmsg = 'Caught IOError for file %s: %s' % (e.filename, e.strerror)
  964. retval = -1
  965. return (retval, errmsg)
  966. def get_adb_devices(self):
  967. """Get the set of attached devices.
  968. Returns:
  969. (device_list, command_output) where device_list is a list of AdbDevice
  970. instances, one for each attached device and command_output is the raw
  971. output of the ADB command.
  972. """
  973. out = self.run_subprocess(
  974. '%s devices -l' % self._find_binary(BuildEnvironment.ADB),
  975. capture=True, shell=True)[0]
  976. devices = []
  977. lines = out.splitlines()
  978. start_line = 0
  979. for i, line in enumerate(lines):
  980. if _MATCH_DEVICES.match(line):
  981. start_line = i + 1
  982. break
  983. if start_line:
  984. for device_line in lines[start_line:]:
  985. if device_line:
  986. devices.append(AdbDevice(device_line))
  987. return (devices, out)
  988. def check_adb_devices(self, adb_device=None):
  989. """Gets the only attached device, or the attached device matching a serial.
  990. When using adb to connect to a device, adb's behavior changes depending on
  991. how many devices are connected. If there is only one device connected, then
  992. no device needs to be specified (as the only device will be used). If more
  993. than one device is connected and no device is specified, adb will error out
  994. as it does not know which device to connect to.
  995. This method ensures that for either case enough valid information is
  996. specified, and returns an instance of AdbDevice representing the valid
  997. device.
  998. Args:
  999. adb_device: The serial to match a device on.
  1000. Returns:
  1001. The only AdbDevice connected to adb, or AdbDevice matching the serial.
  1002. Raises:
  1003. AdbError: More than one attached device and no serial specified, or
  1004. device with matching serial specified cannot be found.
  1005. """
  1006. devices, out = self.get_adb_devices()
  1007. number_of_devices = len(devices)
  1008. if number_of_devices == 0:
  1009. raise common.AdbError('No Android devices are connected to this host.')
  1010. if adb_device:
  1011. devices = [d for d in devices if d.serial == adb_device]
  1012. if not devices:
  1013. raise common.AdbError(
  1014. '%s not found in the list of devices returned by "adb devices -l".'
  1015. 'The devices connected are: %s' % (adb_device, os.linesep + out))
  1016. elif number_of_devices > 1:
  1017. raise common.AdbError(
  1018. 'Multiple Android devices are connected to this host and none were '
  1019. 'specified. The devices connected are: %s' % (os.linesep + out))
  1020. return devices[0]
  1021. def get_adb_device_argument(self, adb_device=None):
  1022. """Construct the argument for ADB to select the specified device.
  1023. Args:
  1024. adb_device: Serial of the device to use with ADB.
  1025. Returns:
  1026. A string which contains the second argument passed to ADB to select a
  1027. target device.
  1028. """
  1029. return '-s ' + adb_device if adb_device else ''
  1030. def list_installed_packages(self, adb_device=None):
  1031. """Get the list of packages installed on an Android device.
  1032. Args:
  1033. adb_device: The serial of the device to query.
  1034. Returns:
  1035. List of package strings.
  1036. Raises:
  1037. AdbError: If it's not possible to query the device.
  1038. """
  1039. packages = []
  1040. for line in self.run_subprocess(
  1041. '%s %s shell pm list packages' % (
  1042. self._find_binary(BuildEnvironment.ADB),
  1043. self.get_adb_device_argument(adb_device)),
  1044. shell=True, capture=True)[0].splitlines():
  1045. m = _MATCH_PACKAGE.match(line)
  1046. if m:
  1047. packages.append(m.groups()[0])
  1048. return packages
  1049. def get_adb_device_name(self, device):
  1050. """Get the string which describes an AdbDevice based upon the verbose mode.
  1051. Args:
  1052. device: AdbDevice instance.
  1053. Returns:
  1054. String which describes the device.
  1055. """
  1056. return str(device) if self.verbose else device.serial
  1057. def install_android_apk(self, path='.', adb_device=None, force_install=True):
  1058. """Install an android apk on the given device.
  1059. This function will attempt to install an unsigned APK if a signed APK is
  1060. not available which will *only* work on rooted devices.
  1061. Args:
  1062. path: Relative path from project directory to project to run.
  1063. adb_device: The serial of the device to run the apk on. If None it will
  1064. the only device connected will be used.
  1065. force_install: Whether to install the package if it's older than the
  1066. package on the target device.
  1067. Raises:
  1068. ConfigurationError: If no APKs are found.
  1069. AdbError: If it's not possible to install the APK.
  1070. """
  1071. adb_path = self._find_binary(BuildEnvironment.ADB)
  1072. device = self.check_adb_devices(adb_device=adb_device)
  1073. adb_device_arg = self.get_adb_device_argument(adb_device=device.serial)
  1074. try:
  1075. manifest = self.parse_manifest(path=path)
  1076. except AndroidManifest.MissingActivityError as e:
  1077. print >>sys.stderr, str(e)
  1078. return
  1079. buildxml = self.create_update_build_xml(manifest, path=path)
  1080. apks = [f for f in self.get_apk_filenames(buildxml.project_name,
  1081. path=path) if os.path.exists(f)]
  1082. if not apks:
  1083. raise common.ConfigurationError(
  1084. 'Unable to find an APK for the project in %s' % (
  1085. self.get_project_directory(path=path)))
  1086. print 'Installing %s on %s' % (apks[0], self.get_adb_device_name(device))
  1087. # If the project is installed and it's older than the current APK,
  1088. # uninstall it.
  1089. if manifest.package_name in self.list_installed_packages(
  1090. adb_device=adb_device):
  1091. if not force_install:
  1092. # Get the modification time of the package on the device.
  1093. get_package_modification_date_args = [adb_path]
  1094. if adb_device_arg:
  1095. get_package_modification_date_args.extend(adb_device_arg.split())
  1096. get_package_modification_date_args.extend([
  1097. 'shell',
  1098. r'f=( $(ls -l $( IFS=":"; p=( $(pm path %s) ); echo ${p[1]} )) ); '
  1099. r'echo "${f[4]} ${f[5]}"' % manifest.package_name])
  1100. out, _ = self.run_subprocess(get_package_modification_date_args,
  1101. capture=True)
  1102. if out:
  1103. remote_modification_time = int(time.mktime(
  1104. datetime.datetime.strptime(out.splitlines()[0],
  1105. '%Y-%m-%d %H:%M').timetuple()))
  1106. local_modification_time = os.stat(apks[0]).st_mtime
  1107. if local_modification_time < remote_modification_time:
  1108. print 'Not installing %s, already up to