From 45ee68fdd427abc4c17c8065aca0abf36d0def0e Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 20 May 2020 17:45:53 +0100 Subject: [PATCH] utils/android: Add support for using aapt2 aapt is now depreciated in favour of aapt2 therefore prefer using the newer binary if it is found on the system. If not present fallback to the old implementation. Currently all invocations of aapt within devlib are compatible with aapt2 however expose the `aapt_version` attribute to indicate which version has been selected to allow for allow maintaining future compatibility. --- devlib/utils/android.py | 67 +++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/devlib/utils/android.py b/devlib/utils/android.py index 1643b7a..3a978c8 100755 --- a/devlib/utils/android.py +++ b/devlib/utils/android.py @@ -46,6 +46,7 @@ logger = logging.getLogger('android') MAX_ATTEMPTS = 5 AM_START_ERROR = re.compile(r"Error: Activity.*") +AAPT_BADGING_OUTPUT = re.compile(r"no dump ((file)|(apk)) specified", re.IGNORECASE) # See: # http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels @@ -93,6 +94,7 @@ android_home = None platform_tools = None adb = None aapt = None +aapt_version = None fastboot = None @@ -201,7 +203,9 @@ class ApkInfo(object): @property def activities(self): if self._activities is None: - cmd = [aapt, 'dump', 'xmltree', self._apk_path, + file_flag = '--file' if aapt_version == 2 else '' + cmd = [aapt, 'dump', 'xmltree', + self._apk_path, '{}'.format(file_flag), 'AndroidManifest.xml'] matched_activities = self.activity_regex.finditer(self._run(cmd)) self._activities = [m.group('name') for m in matched_activities] @@ -644,6 +648,7 @@ class _AndroidEnvironment(object): self.build_tools = None self.adb = None self.aapt = None + self.aapt_version = None self.fastboot = None @@ -676,31 +681,66 @@ def _init_common(env): def _discover_build_tools(env): logger.debug('ANDROID_HOME: {}'.format(env.android_home)) build_tools_directory = os.path.join(env.android_home, 'build-tools') - if not os.path.isdir(build_tools_directory): - msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install - (cannot find build-tools)''' - raise HostError(msg.format(env.android_home)) - env.build_tools = build_tools_directory + if os.path.isdir(build_tools_directory): + env.build_tools = build_tools_directory + +def _check_supported_aapt2(binary): + # At time of writing the version argument of aapt2 is not helpful as + # the output is only a placeholder that does not distinguish between versions + # with and without support for badging. Unfortunately aapt has been + # deprecated and fails to parse some valid apks so we will try to favour + # aapt2 if possible else will fall back to aapt. + # Try to execute the badging command and check if we get an expected error + # message as opposed to an unknown command error to determine if we have a + # suitable version. + cmd = '{} dump badging'.format(binary) + result = subprocess.run(cmd.encode('utf-8'), shell=True, stderr=subprocess.PIPE) + supported = bool(AAPT_BADGING_OUTPUT.search(result.stderr.decode('utf-8'))) + msg = 'Found a {} aapt2 binary at: {}' + logger.debug(msg.format('supported' if supported else 'unsupported', binary)) + return supported def _discover_aapt(env): if env.build_tools: + aapt_path = '' + aapt2_path = '' versions = os.listdir(env.build_tools) for version in reversed(sorted(versions)): - aapt_path = os.path.join(env.build_tools, version, 'aapt') - if os.path.isfile(aapt_path): - logger.debug('Using aapt for version {}'.format(version)) - env.aapt = aapt_path + if not aapt2_path and not os.path.isfile(aapt2_path): + aapt2_path = os.path.join(env.build_tools, version, 'aapt2') + if not aapt_path and not os.path.isfile(aapt_path): + aapt_path = os.path.join(env.build_tools, version, 'aapt') + aapt_version = 1 break + # Use aapt2 only if present and we have a suitable version + if aapt2_path and _check_supported_aapt2(aapt2_path): + aapt_path = aapt2_path + aapt_version = 2 + + # Use the aapt version discoverted from build tools. + if aapt_path: + logger.debug('Using {} for version {}'.format(aapt_path, version)) + env.aapt = aapt_path + env.aapt_version = aapt_version + return + + # Try detecting aapt2 and aapt from PATH if not env.aapt: - env.aapt = which(aapt) + aapt2_path = which('aapt2') + if _check_supported_aapt2(aapt2_path): + env.aapt = aapt2_path + env.aapt_version = 2 + else: + env.aapt = which('aapt') + env.aapt_version = 1 if not env.aapt: - raise HostError('aapt not found. Please make sure it is avaliable in PATH' + raise HostError('aapt/aapt2 not found. Please make sure it is avaliable in PATH' ' or at least one Android platform is installed') def _check_env(): - global android_home, platform_tools, adb, aapt # pylint: disable=W0603 + global android_home, platform_tools, adb, aapt, aapt_version # pylint: disable=W0603 if not android_home: android_home = os.getenv('ANDROID_HOME') if android_home: @@ -711,6 +751,7 @@ def _check_env(): platform_tools = _env.platform_tools adb = _env.adb aapt = _env.aapt + aapt_version = _env.aapt_version class LogcatMonitor(object): """