diff --git a/examples/config.py b/examples/config.py index 44d73e01..f1c6f7b9 100644 --- a/examples/config.py +++ b/examples/config.py @@ -9,7 +9,7 @@ # Override the path to the Android NDK, $ANDROID_NDK by default # ndk_path = "/path/to/android-ndk" # Build tools version to be used -build_tools = "21.1.2" +# build_tools = "21.1.2" # Command for running Ant # ant = "/path/to/ant" diff --git a/fdroidserver/build.py b/fdroidserver/build.py index 9c8acfa2..498cd413 100644 --- a/fdroidserver/build.py +++ b/fdroidserver/build.py @@ -35,7 +35,7 @@ import logging import common import metadata -from common import FDroidException, BuildException, VCSException, FDroidPopen, SilentPopen +from common import FDroidException, BuildException, VCSException, FDroidPopen, SdkToolsPopen try: import paramiko @@ -754,7 +754,7 @@ def build_local(app, thisbuild, vcs, build_dir, output_dir, srclib_dir, extlib_d if not os.path.exists(src): raise BuildException("Unsigned apk is not at expected location of " + src) - p = SilentPopen([config['aapt'], 'dump', 'badging', src]) + p = SdkToolsPopen(['aapt', 'dump', 'badging', src]) vercode = None version = None diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 3a42ec58..9e5189ea 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -130,38 +130,6 @@ def read_config(opts, config_file='config.py'): fill_config_defaults(config) - if not test_sdk_exists(config): - sys.exit(3) - - if not test_build_tools_exists(config): - sys.exit(3) - - bin_paths = { - 'aapt': [ - os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'aapt'), - ], - 'zipalign': [ - os.path.join(config['sdk_path'], 'tools', 'zipalign'), - os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'zipalign'), - ], - 'android': [ - os.path.join(config['sdk_path'], 'tools', 'android'), - ], - 'adb': [ - os.path.join(config['sdk_path'], 'platform-tools', 'adb'), - ], - } - - for b, paths in bin_paths.items(): - config[b] = None - for path in paths: - if os.path.isfile(path): - config[b] = path - break - if config[b] is None: - logging.warn("Could not find %s in any of the following paths:\n%s" % ( - b, '\n'.join(paths))) - # There is no standard, so just set up the most common environment # variables env = os.environ @@ -197,7 +165,45 @@ def read_config(opts, config_file='config.py'): return config +def find_sdk_tools_cmd(cmd): + '''find a working path to a tool from the Android SDK''' + + tooldirs = [] + if config is not None and 'sdk_path' in config and os.path.exists(config['sdk_path']): + # try to find a working path to this command, in all the recent possible paths + if 'build_tools' in config: + build_tools = os.path.join(config['sdk_path'], 'build-tools') + # if 'build_tools' was manually set and exists, check only that one + configed_build_tools = os.path.join(build_tools, config['build_tools']) + if os.path.exists(configed_build_tools): + tooldirs.append(configed_build_tools) + else: + # no configed version, so hunt known paths for it + for f in sorted(os.listdir(build_tools), reverse=True): + if os.path.isdir(os.path.join(build_tools, f)): + tooldirs.append(os.path.join(build_tools, f)) + tooldirs.append(build_tools) + sdk_tools = os.path.join(config['sdk_path'], 'tools') + if os.path.exists(sdk_tools): + tooldirs.append(sdk_tools) + sdk_platform_tools = os.path.join(config['sdk_path'], 'platform-tools') + if os.path.exists(sdk_platform_tools): + tooldirs.append(sdk_platform_tools) + tooldirs.append('/usr/bin') + for d in tooldirs: + if os.path.isfile(os.path.join(d, cmd)): + return os.path.join(d, cmd) + # did not find the command, exit with error message + ensure_build_tools_exists(config) + + def test_sdk_exists(thisconfig): + if 'sdk_path' not in thisconfig: + if 'aapt' in thisconfig and os.path.isfile(thisconfig['aapt']): + return True + else: + logging.error("'sdk_path' not set in config.py!") + return False if thisconfig['sdk_path'] == default_config['sdk_path']: logging.error('No Android SDK found!') logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:') @@ -217,16 +223,15 @@ def test_sdk_exists(thisconfig): return True -def test_build_tools_exists(thisconfig): +def ensure_build_tools_exists(thisconfig): if not test_sdk_exists(thisconfig): - return False + sys.exit(3) build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools') versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools']) if not os.path.isdir(versioned_build_tools): logging.critical('Android Build Tools path "' + versioned_build_tools + '" does not exist!') - return False - return True + sys.exit(3) def write_password_file(pwtype, password=None): @@ -1350,8 +1355,8 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver= # Generate (or update) the ant build file, build.xml... if build['update'] and build['update'] != ['no'] and build['type'] == 'ant': - parms = [config['android'], 'update', 'lib-project'] - lparms = [config['android'], 'update', 'project'] + parms = ['android', 'update', 'lib-project'] + lparms = ['android', 'update', 'project'] if build['target']: parms += ['-t', build['target']] @@ -1369,7 +1374,7 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver= else: logging.debug("Updating subproject %s" % d) cmd = lparms + ['-p', d] - p = FDroidPopen(cmd, cwd=root_dir) + p = SdkToolsPopen(cmd, cwd=root_dir) # Check to see whether an error was returned without a proper exit # code (this is the case for the 'no target set or target invalid' # error) @@ -1600,9 +1605,7 @@ def isApkDebuggable(apkfile, config): :param apkfile: full path to the apk to check""" - p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools', - config['build_tools'], 'aapt'), - 'dump', 'xmltree', apkfile, 'AndroidManifest.xml']) + p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml']) if p.returncode != 0: logging.critical("Failed to get apk manifest information") sys.exit(1) @@ -1641,6 +1644,14 @@ class PopenResult: output = '' +def SdkToolsPopen(commands, cwd=None, shell=False): + cmd = commands[0] + if cmd not in config: + config[cmd] = find_sdk_tools_cmd(commands[0]) + return FDroidPopen([config[cmd]] + commands[1:], + cwd=cwd, shell=shell, output=False) + + def SilentPopen(commands, cwd=None, shell=False): return FDroidPopen(commands, cwd=cwd, shell=shell, output=False) diff --git a/fdroidserver/init.py b/fdroidserver/init.py index 6f96a24b..a0819129 100644 --- a/fdroidserver/init.py +++ b/fdroidserver/init.py @@ -124,6 +124,7 @@ def main(): prefix = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) examplesdir = prefix + '/examples' + aapt = None fdroiddir = os.getcwd() test_config = dict() common.fill_config_defaults(test_config) @@ -133,21 +134,28 @@ def main(): if options.android_home is not None: test_config['sdk_path'] = options.android_home elif not common.test_sdk_exists(test_config): - # if neither --android-home nor the default sdk_path exist, prompt the user - default_sdk_path = '/opt/android-sdk' - while not options.no_prompt: - try: - s = raw_input('Enter the path to the Android SDK (' - + default_sdk_path + ') here:\n> ') - except KeyboardInterrupt: - print('') - sys.exit(1) - if re.match('^\s*$', s) is not None: - test_config['sdk_path'] = default_sdk_path - else: - test_config['sdk_path'] = s - if common.test_sdk_exists(test_config): - break + if os.path.isfile('/usr/bin/aapt'): + # remove sdk_path and build_tools, they are not required + test_config.pop('sdk_path', None) + test_config.pop('build_tools', None) + # make sure at least aapt is found, since this can't do anything without it + test_config['aapt'] = common.find_sdk_tools_cmd('aapt') + else: + # if neither --android-home nor the default sdk_path exist, prompt the user + default_sdk_path = '/opt/android-sdk' + while not options.no_prompt: + try: + s = raw_input('Enter the path to the Android SDK (' + + default_sdk_path + ') here:\n> ') + except KeyboardInterrupt: + print('') + sys.exit(1) + if re.match('^\s*$', s) is not None: + test_config['sdk_path'] = default_sdk_path + else: + test_config['sdk_path'] = s + if common.test_sdk_exists(test_config): + break if not common.test_sdk_exists(test_config): sys.exit(3) @@ -162,34 +170,35 @@ def main(): # "$ANDROID_HOME" may be used if the env var is set up correctly. # If android_home is not None, the path given from the command line # will be directly written in the config. - write_to_config(test_config, 'sdk_path', options.android_home) + if 'sdk_path' in test_config: + write_to_config(test_config, 'sdk_path', options.android_home) else: logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...') logging.info('Try running `fdroid init` in an empty directory.') sys.exit() - # try to find a working aapt, in all the recent possible paths - build_tools = os.path.join(test_config['sdk_path'], 'build-tools') - aaptdirs = [] - aaptdirs.append(os.path.join(build_tools, test_config['build_tools'])) - aaptdirs.append(build_tools) - for f in os.listdir(build_tools): - if os.path.isdir(os.path.join(build_tools, f)): - aaptdirs.append(os.path.join(build_tools, f)) - for d in sorted(aaptdirs, reverse=True): - if os.path.isfile(os.path.join(d, 'aapt')): - aapt = os.path.join(d, 'aapt') - break - if os.path.isfile(aapt): - dirname = os.path.basename(os.path.dirname(aapt)) - if dirname == 'build-tools': - # this is the old layout, before versioned build-tools - test_config['build_tools'] = '' - else: - test_config['build_tools'] = dirname - write_to_config(test_config, 'build_tools') - if not common.test_build_tools_exists(test_config): - sys.exit(3) + if not 'aapt' in test_config or not os.path.isfile(test_config['aapt']): + # try to find a working aapt, in all the recent possible paths + build_tools = os.path.join(test_config['sdk_path'], 'build-tools') + aaptdirs = [] + aaptdirs.append(os.path.join(build_tools, test_config['build_tools'])) + aaptdirs.append(build_tools) + for f in os.listdir(build_tools): + if os.path.isdir(os.path.join(build_tools, f)): + aaptdirs.append(os.path.join(build_tools, f)) + for d in sorted(aaptdirs, reverse=True): + if os.path.isfile(os.path.join(d, 'aapt')): + aapt = os.path.join(d, 'aapt') + break + if os.path.isfile(aapt): + dirname = os.path.basename(os.path.dirname(aapt)) + if dirname == 'build-tools': + # this is the old layout, before versioned build-tools + test_config['build_tools'] = '' + else: + test_config['build_tools'] = dirname + write_to_config(test_config, 'build_tools') + common.ensure_build_tools_exists(test_config) # now that we have a local config.py, read configuration... config = common.read_config(options) @@ -275,7 +284,8 @@ def main(): logging.info('Built repo based in "' + fdroiddir + '"') logging.info('with this config:') logging.info(' Android SDK:\t\t\t' + config['sdk_path']) - logging.info(' Android SDK Build Tools:\t' + os.path.dirname(aapt)) + if aapt: + logging.info(' Android SDK Build Tools:\t' + os.path.dirname(aapt)) logging.info(' Android NDK (optional):\t' + ndk_path) logging.info(' Keystore for signing key:\t' + keystore) if repo_keyalias is not None: diff --git a/fdroidserver/install.py b/fdroidserver/install.py index f6862e5a..a5cb98ad 100644 --- a/fdroidserver/install.py +++ b/fdroidserver/install.py @@ -25,14 +25,14 @@ from optparse import OptionParser, OptionError import logging import common -from common import FDroidPopen, FDroidException +from common import SdkToolsPopen, FDroidException options = None config = None def devices(): - p = FDroidPopen([config['adb'], "devices"]) + p = SdkToolsPopen(['adb', "devices"]) if p.returncode != 0: raise FDroidException("An error occured when finding devices: %s" % p.output) lines = p.output.splitlines() @@ -103,7 +103,7 @@ def main(): logging.info("Installing %s..." % apk) for dev in devs: logging.info("Installing %s on %s..." % (apk, dev)) - p = FDroidPopen([config['adb'], "-s", dev, "install", apk]) + p = SdkToolsPopen(['adb', "-s", dev, "install", apk]) fail = "" for line in p.output.splitlines(): if line.startswith("Failure"): diff --git a/fdroidserver/publish.py b/fdroidserver/publish.py index 39f601c4..48d0503e 100644 --- a/fdroidserver/publish.py +++ b/fdroidserver/publish.py @@ -28,7 +28,7 @@ import logging import common import metadata -from common import FDroidPopen, BuildException +from common import FDroidPopen, SdkToolsPopen, BuildException config = None options = None @@ -213,8 +213,8 @@ def main(): raise BuildException("Failed to sign application") # Zipalign it... - p = FDroidPopen([config['zipalign'], '-v', '4', apkfile, - os.path.join(output_dir, apkfilename)]) + p = SdkToolsPopen(['zipalign', '-v', '4', apkfile, + os.path.join(output_dir, apkfilename)]) if p.returncode != 0: raise BuildException("Failed to align application") os.remove(apkfile) diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 0339f460..eb04ddbb 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -39,7 +39,7 @@ import logging import common import metadata -from common import FDroidPopen, SilentPopen +from common import FDroidPopen, SdkToolsPopen from metadata import MetaDataException @@ -436,7 +436,7 @@ def scan_apks(apps, apkcache, repodir, knownapks): thisinfo['features'] = set() thisinfo['icons_src'] = {} thisinfo['icons'] = {} - p = SilentPopen([config['aapt'], 'dump', 'badging', apkfile]) + p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile]) if p.returncode != 0: if options.delete_unknown: if os.path.exists(apkfile): diff --git a/tests/common.TestCase b/tests/common.TestCase new file mode 100755 index 00000000..d2066378 --- /dev/null +++ b/tests/common.TestCase @@ -0,0 +1,95 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# http://www.drdobbs.com/testing/unit-testing-with-python/240165163 + +import inspect +import optparse +import os +import sys +import unittest + +localmodule = os.path.realpath(os.path.join( + os.path.dirname(inspect.getfile(inspect.currentframe())), + '..')) +print('localmodule: ' + localmodule) +if localmodule not in sys.path: + sys.path.insert(0,localmodule) + +import fdroidserver.common + +class CommonTest(unittest.TestCase): + '''fdroidserver/common.py''' + + def _set_build_tools(self): + build_tools = os.path.join(fdroidserver.common.config['sdk_path'], 'build-tools') + if os.path.exists(build_tools): + fdroidserver.common.config['build_tools'] = '' + for f in sorted(os.listdir(build_tools), reverse=True): + versioned = os.path.join(build_tools, f) + if os.path.isdir(versioned) \ + and os.path.isfile(os.path.join(versioned, 'aapt')): + fdroidserver.common.config['build_tools'] = versioned + break + return True + else: + print 'no build-tools found: ' + build_tools + return False + + def _find_all(self): + for cmd in ('aapt', 'adb', 'android', 'zipalign'): + path = fdroidserver.common.find_sdk_tools_cmd(cmd) + if path is not None: + self.assertTrue(os.path.exists(path)) + self.assertTrue(os.path.isfile(path)) + + def test_find_sdk_tools_cmd(self): + fdroidserver.common.config = dict() + # TODO add this once everything works without sdk_path set in config + #self._find_all() + sdk_path = os.getenv('ANDROID_HOME') + if os.path.exists(sdk_path): + fdroidserver.common.config['sdk_path'] = sdk_path + if os.path.exists('/usr/bin/aapt'): + # this test only works when /usr/bin/aapt is installed + self._find_all() + build_tools = os.path.join(sdk_path, 'build-tools') + if self._set_build_tools(): + self._find_all() + else: + print 'no build-tools found: ' + build_tools + + def testIsApkDebuggable(self): + config = dict() + config['sdk_path'] = os.getenv('ANDROID_HOME') + fdroidserver.common.config = config + self._set_build_tools(); + config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt') + # these are set debuggable + testfiles = [] + testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip.apk')) + testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-badsig.apk')) + testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-badcert.apk')) + for apkfile in testfiles: + debuggable = fdroidserver.common.isApkDebuggable(apkfile, config) + self.assertTrue(debuggable, + "debuggable APK state was not properly parsed!") + # these are set NOT debuggable + testfiles = [] + testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-release.apk')) + testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-release-unsigned.apk')) + for apkfile in testfiles: + debuggable = fdroidserver.common.isApkDebuggable(apkfile, config) + self.assertFalse(debuggable, + "debuggable APK state was not properly parsed!") + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option("-v", "--verbose", action="store_true", default=False, + help="Spew out even more information than normal") + (fdroidserver.common.options, args) = parser.parse_args(['--verbose']) + + newSuite = unittest.TestSuite() + newSuite.addTest(unittest.makeSuite(CommonTest)) + unittest.main() diff --git a/tests/install.TestCase b/tests/install.TestCase new file mode 100755 index 00000000..fcedb959 --- /dev/null +++ b/tests/install.TestCase @@ -0,0 +1,46 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# http://www.drdobbs.com/testing/unit-testing-with-python/240165163 + +import inspect +import optparse +import os +import sys +import unittest + +localmodule = os.path.realpath(os.path.join( + os.path.dirname(inspect.getfile(inspect.currentframe())), + '..')) +print('localmodule: ' + localmodule) +if localmodule not in sys.path: + sys.path.insert(0,localmodule) + +import fdroidserver.common +import fdroidserver.install + +class InstallTest(unittest.TestCase): + '''fdroidserver/install.py''' + + def test_devices(self): + config = dict() + config['sdk_path'] = os.getenv('ANDROID_HOME') + fdroidserver.common.config = config + config['adb'] = fdroidserver.common.find_sdk_tools_cmd('adb') + self.assertTrue(os.path.exists(config['adb'])) + self.assertTrue(os.path.isfile(config['adb'])) + devices = fdroidserver.install.devices() + self.assertIsInstance(devices, list, 'install.devices() did not return a list!') + for device in devices: + self.assertIsInstance(device, basestring) + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option("-v", "--verbose", action="store_true", default=False, + help="Spew out even more information than normal") + (fdroidserver.install.options, args) = parser.parse_args(['--verbose']) + + newSuite = unittest.TestSuite() + newSuite.addTest(unittest.makeSuite(InstallTest)) + unittest.main() diff --git a/tests/run-tests b/tests/run-tests index ae566b74..d1f988fa 100755 --- a/tests/run-tests +++ b/tests/run-tests @@ -97,8 +97,9 @@ echo_header "test python getsig replacement" cd $WORKSPACE/tests/getsig ./make.sh -cd $WORKSPACE/tests -./update.TestCase +for testcase in $WORKSPACE/tests/*.TestCase; do + $testcase +done #------------------------------------------------------------------------------# @@ -208,16 +209,20 @@ $fdroid init --keystore $KEYSTORE --android-home $FAKE_ANDROID_HOME #------------------------------------------------------------------------------# echo_header "check that 'fdroid init' fails when build-tools cannot be found" -REPOROOT=`create_test_dir` -FAKE_ANDROID_HOME=`create_test_dir` -create_fake_android_home $FAKE_ANDROID_HOME -rm -f $FAKE_ANDROID_HOME/build-tools/*/aapt -KEYSTORE=$REPOROOT/keystore.jks -cd $REPOROOT -set +e -$fdroid init --keystore $KEYSTORE --android-home $FAKE_ANDROID_HOME -[ $? -eq 0 ] && exit 1 -set -e +if [ -e /usr/bin/aapt ]; then + echo "/usr/bin/aapt exists, not running test" +else + REPOROOT=`create_test_dir` + FAKE_ANDROID_HOME=`create_test_dir` + create_fake_android_home $FAKE_ANDROID_HOME + rm -f $FAKE_ANDROID_HOME/build-tools/*/aapt + KEYSTORE=$REPOROOT/keystore.jks + cd $REPOROOT + set +e + $fdroid init --keystore $KEYSTORE --android-home $FAKE_ANDROID_HOME + [ $? -eq 0 ] && exit 1 + set -e +fi #------------------------------------------------------------------------------# diff --git a/tests/urzip-release-unsigned.apk b/tests/urzip-release-unsigned.apk new file mode 100644 index 00000000..7bc22294 Binary files /dev/null and b/tests/urzip-release-unsigned.apk differ diff --git a/tests/urzip-release.apk b/tests/urzip-release.apk new file mode 100644 index 00000000..28a03450 Binary files /dev/null and b/tests/urzip-release.apk differ