chiark / gitweb /
Apply some autopep8-python2 suggestions
[fdroidserver.git] / fdroidserver / common.py
index e2e64cebe4d253009d4b88f426d3f0244ccd7df5..b9e7b81c615926c69f73fc45ae2bfe30228ac3e6 100644 (file)
@@ -36,29 +36,60 @@ import metadata
 
 config = None
 options = None
-
-
-def get_default_config():
-    return {
-        'sdk_path': os.getenv("ANDROID_HOME"),
-        'ndk_path': os.getenv("ANDROID_NDK"),
-        'build_tools': "19.1.0",
-        'ant': "ant",
-        'mvn3': "mvn",
-        'gradle': 'gradle',
-        'archive_older': 0,
-        'update_stats': False,
-        'stats_to_carbon': False,
-        'repo_maxage': 0,
-        'build_server_always': False,
-        'keystore': os.path.join(os.getenv("HOME"), '.local', 'share', 'fdroidserver', 'keystore.jks'),
-        'smartcardoptions': [],
-        'char_limits': {
-            'Summary': 50,
-            'Description': 1500
-        },
-        'keyaliases': {},
-    }
+env = None
+
+
+default_config = {
+    'sdk_path': "$ANDROID_HOME",
+    'ndk_path': "$ANDROID_NDK",
+    'build_tools': "21.1.2",
+    'ant': "ant",
+    'mvn3': "mvn",
+    'gradle': 'gradle',
+    'sync_from_local_copy_dir': False,
+    'make_current_version_link': True,
+    'current_version_name_source': 'Name',
+    'update_stats': False,
+    'stats_ignore': [],
+    'stats_server': None,
+    'stats_user': None,
+    'stats_to_carbon': False,
+    'repo_maxage': 0,
+    'build_server_always': False,
+    'keystore': os.path.join("$HOME", '.local', 'share', 'fdroidserver', 'keystore.jks'),
+    'smartcardoptions': [],
+    'char_limits': {
+        'Summary': 50,
+        'Description': 1500
+    },
+    'keyaliases': {},
+    'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
+    'repo_name': "My First FDroid Repo Demo",
+    'repo_icon': "fdroid-icon.png",
+    'repo_description': '''
+        This is a repository of apps to be used with FDroid. Applications in this
+        repository are either official binaries built by the original application
+        developers, or are binaries built from source by the admin of f-droid.org
+        using the tools on https://gitlab.com/u/fdroid.
+        ''',
+    'archive_older': 0,
+}
+
+
+def fill_config_defaults(thisconfig):
+    for k, v in default_config.items():
+        if k not in thisconfig:
+            thisconfig[k] = v
+
+    # Expand paths (~users and $vars)
+    for k in ['sdk_path', 'ndk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']:
+        v = thisconfig[k]
+        orig = v
+        v = os.path.expanduser(v)
+        v = os.path.expandvars(v)
+        if orig != v:
+            thisconfig[k] = v
+            thisconfig[k + '_orig'] = orig
 
 
 def read_config(opts, config_file='config.py'):
@@ -67,7 +98,7 @@ def read_config(opts, config_file='config.py'):
     The config is read from config_file, which is in the current directory when
     any of the repo management commands are used.
     """
-    global config, options
+    global config, options, env
 
     if config is not None:
         return config
@@ -97,72 +128,110 @@ def read_config(opts, config_file='config.py'):
         if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
             logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
 
-    defconfig = get_default_config()
-    for k, v in defconfig.items():
-        if k not in config:
-            config[k] = v
+    fill_config_defaults(config)
 
-    # Expand environment variables
-    for k, v in config.items():
-        if type(v) != str:
-            continue
-        v = os.path.expanduser(v)
-        config[k] = os.path.expandvars(v)
-
-    if not test_sdk_exists(config):
-        sys.exit(3)
+    # There is no standard, so just set up the most common environment
+    # variables
+    env = os.environ
+    for n in ['ANDROID_HOME', 'ANDROID_SDK']:
+        env[n] = config['sdk_path']
+    for n in ['ANDROID_NDK', 'NDK']:
+        env[n] = config['ndk_path']
 
     for k in ["keystorepass", "keypass"]:
         if k in config:
             write_password_file(k)
 
-    # since this is used with rsync, where trailing slashes have meaning,
-    # ensure there is always a trailing slash
+    for k in ["repo_description", "archive_description"]:
+        if k in config:
+            config[k] = clean_description(config[k])
+
     if 'serverwebroot' in config:
-        if config['serverwebroot'][-1] != '/':
-            config['serverwebroot'] += '/'
-        config['serverwebroot'] = config['serverwebroot'].replace('//', '/')
+        if isinstance(config['serverwebroot'], basestring):
+            roots = [config['serverwebroot']]
+        elif all(isinstance(item, basestring) for item in config['serverwebroot']):
+            roots = config['serverwebroot']
+        else:
+            raise TypeError('only accepts strings, lists, and tuples')
+        rootlist = []
+        for rootstr in roots:
+            # since this is used with rsync, where trailing slashes have
+            # meaning, ensure there is always a trailing slash
+            if rootstr[-1] != '/':
+                rootstr += '/'
+            rootlist.append(rootstr.replace('//', '/'))
+        config['serverwebroot'] = rootlist
 
     return config
 
 
-def test_sdk_exists(c):
-    if c['sdk_path'] is None:
-        # c['sdk_path'] is set to the value of ANDROID_HOME by default
-        logging.critical('No Android SDK found! ANDROID_HOME is not set and sdk_path is not in config.py!')
-        logging.info('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
-        logging.info('\texport ANDROID_HOME=/opt/android-sdk')
-        return False
-    if not os.path.exists(c['sdk_path']):
-        logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
-        return False
-    if not os.path.isdir(c['sdk_path']):
-        logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
+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.:')
+        logging.error('\texport ANDROID_HOME=/opt/android-sdk')
         return False
-    if not os.path.isdir(os.path.join(c['sdk_path'], 'build-tools')):
-        logging.critical('Android SDK path "' + c['sdk_path'] + '" does not contain "build-tools/"!')
+    if not os.path.exists(thisconfig['sdk_path']):
+        logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" does not exist!')
         return False
-    if not os.path.isdir(os.path.join(c['sdk_path'], 'build-tools', c['build_tools'])):
-        logging.critical('Configured build-tools version "' + c['build_tools'] + '" not found in the SDK!')
+    if not os.path.isdir(thisconfig['sdk_path']):
+        logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" is not a directory!')
         return False
+    for d in ['build-tools', 'platform-tools', 'tools']:
+        if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
+            logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
+                thisconfig['sdk_path'], d))
+            return False
     return True
 
 
-def test_build_tools_exists(c):
-    if not test_sdk_exists(c):
-        return False
-    build_tools = os.path.join(c['sdk_path'], 'build-tools')
-    versioned_build_tools = os.path.join(build_tools, c['build_tools'])
+def ensure_build_tools_exists(thisconfig):
+    if not test_sdk_exists(thisconfig):
+        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
-    if not os.path.exists(os.path.join(c['sdk_path'], 'build-tools', c['build_tools'], 'aapt')):
-        logging.critical('Android Build Tools "'
-                         + versioned_build_tools
-                         + '" does not contain "aapt"!')
-        return False
-    return True
+        sys.exit(3)
 
 
 def write_password_file(pwtype, password=None):
@@ -211,32 +280,34 @@ def read_app_args(args, allapps, allow_vercodes=False):
     if not vercodes:
         return allapps
 
-    apps = [app for app in allapps if app['id'] in vercodes]
+    apps = {}
+    for appid, app in allapps.iteritems():
+        if appid in vercodes:
+            apps[appid] = app
 
     if len(apps) != len(vercodes):
-        allids = [app["id"] for app in allapps]
         for p in vercodes:
-            if p not in allids:
+            if p not in allapps:
                 logging.critical("No such package: %s" % p)
-        raise Exception("Found invalid app ids in arguments")
+        raise FDroidException("Found invalid app ids in arguments")
     if not apps:
-        raise Exception("No packages specified")
+        raise FDroidException("No packages specified")
 
     error = False
-    for app in apps:
-        vc = vercodes[app['id']]
+    for appid, app in apps.iteritems():
+        vc = vercodes[appid]
         if not vc:
             continue
         app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
-        if len(app['builds']) != len(vercodes[app['id']]):
+        if len(app['builds']) != len(vercodes[appid]):
             error = True
             allvcs = [b['vercode'] for b in app['builds']]
-            for v in vercodes[app['id']]:
+            for v in vercodes[appid]:
                 if v not in allvcs:
-                    logging.critical("No such vercode %s for app %s" % (v, app['id']))
+                    logging.critical("No such vercode %s for app %s" % (v, appid))
 
     if error:
-        raise Exception("Found invalid vercodes for some apps")
+        raise FDroidException("Found invalid vercodes for some apps")
 
     return apps
 
@@ -249,6 +320,19 @@ def has_extension(filename, extension):
 apk_regex = None
 
 
+def clean_description(description):
+    'Remove unneeded newlines and spaces from a block of description text'
+    returnstring = ''
+    # this is split up by paragraph to make removing the newlines easier
+    for paragraph in re.split(r'\n\n', description):
+        paragraph = re.sub('\r', '', paragraph)
+        paragraph = re.sub('\n', ' ', paragraph)
+        paragraph = re.sub(' {2,}', ' ', paragraph)
+        paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
+        returnstring += paragraph + '\n\n'
+    return returnstring.rstrip('\n')
+
+
 def apknameinfo(filename):
     global apk_regex
     filename = os.path.basename(filename)
@@ -258,7 +342,7 @@ def apknameinfo(filename):
     try:
         result = (m.group(1), m.group(2))
     except AttributeError:
-        raise Exception("Invalid apk name: %s" % filename)
+        raise FDroidException("Invalid apk name: %s" % filename)
     return result
 
 
@@ -285,8 +369,6 @@ def getcvname(app):
 def getvcs(vcstype, remote, local):
     if vcstype == 'git':
         return vcs_git(remote, local)
-    if vcstype == 'svn':
-        return vcs_svn(remote, local)
     if vcstype == 'git-svn':
         return vcs_gitsvn(remote, local)
     if vcstype == 'hg':
@@ -294,9 +376,11 @@ def getvcs(vcstype, remote, local):
     if vcstype == 'bzr':
         return vcs_bzr(remote, local)
     if vcstype == 'srclib':
-        if local != 'build/srclib/' + remote:
+        if local != os.path.join('build', 'srclib', remote):
             raise VCSException("Error: srclib paths are hard-coded!")
-        return getsrclib(remote, 'build/srclib', raw=True)
+        return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
+    if vcstype == 'svn':
+        raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
     raise VCSException("Invalid vcs type " + vcstype)
 
 
@@ -307,11 +391,12 @@ def getsrclibvcs(name):
 
 
 class vcs:
+
     def __init__(self, remote, local):
 
         # svn, git-svn and bzr may require auth
         self.username = None
-        if self.repotype() in ('svn', 'git-svn', 'bzr'):
+        if self.repotype() in ('git-svn', 'bzr'):
             if '@' in remote:
                 self.username, remote = remote.split('@')
                 if ':' not in self.username:
@@ -320,6 +405,7 @@ class vcs:
 
         self.remote = remote
         self.local = local
+        self.clone_failed = False
         self.refreshed = False
         self.srclib = None
 
@@ -335,6 +421,9 @@ class vcs:
     # the repo - otherwise it must specify a valid revision.
     def gotorevision(self, rev):
 
+        if self.clone_failed:
+            raise VCSException("Downloading the repository already failed once, not trying again.")
+
         # The .fdroidvcs-id file for a repo tells us what VCS type
         # and remote that directory was created from, allowing us to drop it
         # automatically if either of those things changes.
@@ -351,20 +440,30 @@ class vcs:
                     writeback = False
                 else:
                     deleterepo = True
-                    logging.info("Repository details changed - deleting")
+                    logging.info("Repository details for %s changed - deleting" % (
+                        self.local))
             else:
                 deleterepo = True
-                logging.info("Repository details missing - deleting")
+                logging.info("Repository details for %s missing - deleting" % (
+                    self.local))
         if deleterepo:
             shutil.rmtree(self.local)
 
-        self.gotorevisionx(rev)
+        exc = None
+
+        try:
+            self.gotorevisionx(rev)
+        except FDroidException, e:
+            exc = e
 
         # If necessary, write the .fdroidvcs file.
-        if writeback:
+        if writeback and not self.clone_failed:
             with open(fdpath, 'w') as f:
                 f.write(cdata)
 
+        if exc is not None:
+            raise exc
+
     # Derived classes need to implement this. It's called once basic checking
     # has been performend.
     def gotorevisionx(self, rev):
@@ -402,8 +501,8 @@ class vcs_git(vcs):
     # fdroidserver) and then we'll proceed to destroy it! This is called as
     # a safety check.
     def checkrepo(self):
-        p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
-        result = p.stdout.rstrip()
+        p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
+        result = p.output.rstrip()
         if not result.endswith(self.local):
             raise VCSException('Repository mismatch')
 
@@ -412,37 +511,49 @@ class vcs_git(vcs):
             # Brand new checkout
             p = FDroidPopen(['git', 'clone', self.remote, self.local])
             if p.returncode != 0:
-                raise VCSException("Git clone failed")
+                self.clone_failed = True
+                raise VCSException("Git clone failed", p.output)
             self.checkrepo()
         else:
             self.checkrepo()
             # Discard any working tree changes
-            p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
+            p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Git reset failed")
+                raise VCSException("Git reset failed", p.output)
             # Remove untracked files now, in case they're tracked in the target
             # revision (it happens!)
-            p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+            p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Git clean failed")
+                raise VCSException("Git clean failed", p.output)
             if not self.refreshed:
                 # Get latest commits and tags from remote
                 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
                 if p.returncode != 0:
-                    raise VCSException("Git fetch failed")
-                p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
+                    raise VCSException("Git fetch failed", p.output)
+                p = FDroidPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local, output=False)
+                if p.returncode != 0:
+                    raise VCSException("Git fetch failed", p.output)
+                # Recreate origin/HEAD as git clone would do it, in case it disappeared
+                p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Git fetch failed")
+                    lines = p.output.splitlines()
+                    if 'Multiple remote HEAD branches' not in lines[0]:
+                        raise VCSException("Git remote set-head failed", p.output)
+                    branch = lines[1].split(' ')[-1]
+                    p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
+                    if p2.returncode != 0:
+                        raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
                 self.refreshed = True
-        # Check out the appropriate revision
-        rev = str(rev if rev else 'origin/master')
-        p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
+        # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
+        # a github repo. Most of the time this is the same as origin/master.
+        rev = rev or 'origin/HEAD'
+        p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Git checkout failed")
+            raise VCSException("Git checkout of '%s' failed" % rev, p.output)
         # Get rid of any uncontrolled files left behind
-        p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+        p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Git clean failed")
+            raise VCSException("Git clean failed", p.output)
 
     def initsubmodules(self):
         self.checkrepo()
@@ -463,28 +574,29 @@ class vcs_git(vcs):
                 ['git', 'reset', '--hard'],
                 ['git', 'clean', '-dffx'],
                 ]:
-            p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
+            p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Git submodule reset failed")
-        p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local)
+                raise VCSException("Git submodule reset failed", p.output)
+        p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Git submodule sync failed")
+            raise VCSException("Git submodule sync failed", p.output)
         p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
         if p.returncode != 0:
-            raise VCSException("Git submodule update failed")
+            raise VCSException("Git submodule update failed", p.output)
 
     def gettags(self):
         self.checkrepo()
-        p = SilentPopen(['git', 'tag'], cwd=self.local)
-        return p.stdout.splitlines()
+        p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False)
+        return p.output.splitlines()
 
     def latesttags(self, alltags, number):
         self.checkrepo()
-        p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | \
-                xargs -I@ git log --format=format:"%at @%n" -1 @ | \
-                sort -n | awk \'{print $2}\''],
-                        cwd=self.local, shell=True)
-        return p.stdout.splitlines()[-number:]
+        p = FDroidPopen(['echo "' + '\n'.join(alltags) + '" | '
+                         +
+                         'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
+                         + 'sort -n | awk \'{print $2}\''],
+                        cwd=self.local, shell=True, output=False)
+        return p.output.splitlines()[-number:]
 
 
 class vcs_gitsvn(vcs):
@@ -504,8 +616,8 @@ class vcs_gitsvn(vcs):
     # fdroidserver) and then we'll proceed to destory it! This is called as
     # a safety check.
     def checkrepo(self):
-        p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
-        result = p.stdout.rstrip()
+        p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
+        result = p.output.rstrip()
         if not result.endswith(self.local):
             raise VCSException('Repository mismatch')
 
@@ -522,126 +634,98 @@ class vcs_gitsvn(vcs):
                         gitsvn_cmd += ' -t %s' % i[5:]
                     elif i.startswith('branches='):
                         gitsvn_cmd += ' -b %s' % i[9:]
-                p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
+                p = FDroidPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Git clone failed")
+                    self.clone_failed = True
+                    raise VCSException("Git svn clone failed", p.output)
             else:
-                p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
+                p = FDroidPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Git clone failed")
+                    self.clone_failed = True
+                    raise VCSException("Git svn clone failed", p.output)
             self.checkrepo()
         else:
             self.checkrepo()
             # Discard any working tree changes
-            p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
+            p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Git reset failed")
+                raise VCSException("Git reset failed", p.output)
             # Remove untracked files now, in case they're tracked in the target
             # revision (it happens!)
-            p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+            p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Git clean failed")
+                raise VCSException("Git clean failed", p.output)
             if not self.refreshed:
                 # Get new commits, branches and tags from repo
-                p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
+                p = FDroidPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True, output=False)
                 if p.returncode != 0:
                     raise VCSException("Git svn fetch failed")
-                p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
+                p = FDroidPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Git svn rebase failed")
+                    raise VCSException("Git svn rebase failed", p.output)
                 self.refreshed = True
 
-        rev = str(rev if rev else 'master')
+        rev = rev or 'master'
         if rev:
             nospaces_rev = rev.replace(' ', '%20')
             # Try finding a svn tag
-            p = SilentPopen(['git', 'checkout', 'tags/' + nospaces_rev], cwd=self.local)
+            for treeish in ['origin/', '']:
+                p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
+                if p.returncode == 0:
+                    break
             if p.returncode != 0:
                 # No tag found, normal svn rev translation
                 # Translate svn rev into git format
                 rev_split = rev.split('/')
-                if len(rev_split) > 1:
-                    treeish = rev_split[0]
-                    svn_rev = rev_split[1]
 
-                else:
-                    # if no branch is specified, then assume trunk (ie. 'master'
-                    # branch):
-                    treeish = 'master'
-                    svn_rev = rev
+                p = None
+                for treeish in ['origin/', '']:
+                    if len(rev_split) > 1:
+                        treeish += rev_split[0]
+                        svn_rev = rev_split[1]
 
-                p = SilentPopen(['git', 'svn', 'find-rev', 'r' + svn_rev, treeish], cwd=self.local)
-                git_rev = p.stdout.rstrip()
+                    else:
+                        # if no branch is specified, then assume trunk (i.e. 'master' branch):
+                        treeish += 'master'
+                        svn_rev = rev
+
+                    svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
+
+                    p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False)
+                    git_rev = p.output.rstrip()
+
+                    if p.returncode == 0 and git_rev:
+                        break
 
                 if p.returncode != 0 or not git_rev:
                     # Try a plain git checkout as a last resort
-                    p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
+                    p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False)
                     if p.returncode != 0:
-                        raise VCSException("No git treeish found and direct git checkout failed")
+                        raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
                 else:
                     # Check out the git rev equivalent to the svn rev
-                    p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
+                    p = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False)
                     if p.returncode != 0:
-                        raise VCSException("Git svn checkout failed")
+                        raise VCSException("Git checkout of '%s' failed" % rev, p.output)
 
         # Get rid of any uncontrolled files left behind
-        p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+        p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Git clean failed")
+            raise VCSException("Git clean failed", p.output)
 
     def gettags(self):
         self.checkrepo()
-        return os.listdir(os.path.join(self.local, '.git/svn/refs/remotes/tags'))
+        for treeish in ['origin/', '']:
+            d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
+            if os.path.isdir(d):
+                return os.listdir(d)
 
     def getref(self):
         self.checkrepo()
-        p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
+        p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False)
         if p.returncode != 0:
             return None
-        return p.stdout.strip()
-
-
-class vcs_svn(vcs):
-
-    def repotype(self):
-        return 'svn'
-
-    def userargs(self):
-        if self.username is None:
-            return ['--non-interactive']
-        return ['--username', self.username,
-                '--password', self.password,
-                '--non-interactive']
-
-    def gotorevisionx(self, rev):
-        if not os.path.exists(self.local):
-            p = SilentPopen(['svn', 'checkout', self.remote, self.local] + self.userargs())
-            if p.returncode != 0:
-                raise VCSException("Svn checkout failed")
-        else:
-            for svncommand in (
-                    'svn revert -R .',
-                    r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
-                p = SilentPopen([svncommand], cwd=self.local, shell=True)
-                if p.returncode != 0:
-                    raise VCSException("Svn reset ({0}) failed in {1}".format(svncommand, self.local))
-            if not self.refreshed:
-                p = SilentPopen(['svn', 'update'] + self.userargs(), cwd=self.local)
-                if p.returncode != 0:
-                    raise VCSException("Svn update failed")
-                self.refreshed = True
-
-        revargs = list(['-r', rev] if rev else [])
-        p = SilentPopen(['svn', 'update', '--force'] + revargs + self.userargs(), cwd=self.local)
-        if p.returncode != 0:
-            raise VCSException("Svn update failed")
-
-    def getref(self):
-        p = SilentPopen(['svn', 'info'], cwd=self.local)
-        for line in p.stdout.splitlines():
-            if line and line.startswith('Last Changed Rev: '):
-                return line[18:]
-        return None
+        return p.output.strip()
 
 
 class vcs_hg(vcs):
@@ -651,39 +735,40 @@ class vcs_hg(vcs):
 
     def gotorevisionx(self, rev):
         if not os.path.exists(self.local):
-            p = SilentPopen(['hg', 'clone', self.remote, self.local])
+            p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False)
             if p.returncode != 0:
-                raise VCSException("Hg clone failed")
+                self.clone_failed = True
+                raise VCSException("Hg clone failed", p.output)
         else:
-            p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
+            p = FDroidPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True, output=False)
             if p.returncode != 0:
-                raise VCSException("Hg clean failed")
+                raise VCSException("Hg clean failed", p.output)
             if not self.refreshed:
-                p = SilentPopen(['hg', 'pull'], cwd=self.local)
+                p = FDroidPopen(['hg', 'pull'], cwd=self.local, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Hg pull failed")
+                    raise VCSException("Hg pull failed", p.output)
                 self.refreshed = True
 
-        rev = str(rev if rev else 'default')
+        rev = rev or 'default'
         if not rev:
             return
-        p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
+        p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Hg checkout failed")
-        p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
+            raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
+        p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
         # Also delete untracked files, we have to enable purge extension for that:
-        if "'purge' is provided by the following extension" in p.stdout:
-            with open(self.local + "/.hg/hgrc", "a") as myfile:
+        if "'purge' is provided by the following extension" in p.output:
+            with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
                 myfile.write("\n[extensions]\nhgext.purge=\n")
-            p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
+            p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("HG purge failed")
+                raise VCSException("HG purge failed", p.output)
         elif p.returncode != 0:
-            raise VCSException("HG purge failed")
+            raise VCSException("HG purge failed", p.output)
 
     def gettags(self):
-        p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
-        return p.stdout.splitlines()[1:]
+        p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
+        return p.output.splitlines()[1:]
 
 
 class vcs_bzr(vcs):
@@ -693,47 +778,48 @@ class vcs_bzr(vcs):
 
     def gotorevisionx(self, rev):
         if not os.path.exists(self.local):
-            p = SilentPopen(['bzr', 'branch', self.remote, self.local])
+            p = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False)
             if p.returncode != 0:
-                raise VCSException("Bzr branch failed")
+                self.clone_failed = True
+                raise VCSException("Bzr branch failed", p.output)
         else:
-            p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
+            p = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False)
             if p.returncode != 0:
-                raise VCSException("Bzr revert failed")
+                raise VCSException("Bzr revert failed", p.output)
             if not self.refreshed:
-                p = SilentPopen(['bzr', 'pull'], cwd=self.local)
+                p = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False)
                 if p.returncode != 0:
-                    raise VCSException("Bzr update failed")
+                    raise VCSException("Bzr update failed", p.output)
                 self.refreshed = True
 
         revargs = list(['-r', rev] if rev else [])
-        p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
+        p = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False)
         if p.returncode != 0:
-            raise VCSException("Bzr revert failed")
+            raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
 
     def gettags(self):
-        p = SilentPopen(['bzr', 'tags'], cwd=self.local)
+        p = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False)
         return [tag.split('   ')[0].strip() for tag in
-                p.stdout.splitlines()]
+                p.output.splitlines()]
 
 
 def retrieve_string(app_dir, string, xmlfiles=None):
 
     res_dirs = [
         os.path.join(app_dir, 'res'),
-        os.path.join(app_dir, 'src/main'),
+        os.path.join(app_dir, 'src', 'main'),
         ]
 
     if xmlfiles is None:
         xmlfiles = []
         for res_dir in res_dirs:
             for r, d, f in os.walk(res_dir):
-                if r.endswith('/values'):
+                if os.path.basename(r) == 'values':
                     xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
 
     string_search = None
     if string.startswith('@string/'):
-        string_search = re.compile(r'.*name="' + string[8:] + '".*?>([^<]+?)<.*').search
+        string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
     elif string.startswith('&') and string.endswith(';'):
         string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
 
@@ -749,7 +835,7 @@ def retrieve_string(app_dir, string, xmlfiles=None):
 
 
 # Return list of existing files that will be used to find the highest vercode
-def manifest_paths(app_dir, flavour):
+def manifest_paths(app_dir, flavours):
 
     possible_manifests = \
         [os.path.join(app_dir, 'AndroidManifest.xml'),
@@ -757,7 +843,9 @@ def manifest_paths(app_dir, flavour):
          os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
          os.path.join(app_dir, 'build.gradle')]
 
-    if flavour:
+    for flavour in flavours:
+        if flavour == 'yes':
+            continue
         possible_manifests.append(
             os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
 
@@ -765,11 +853,11 @@ def manifest_paths(app_dir, flavour):
 
 
 # Retrieve the package name. Returns the name, or None if not found.
-def fetch_real_name(app_dir, flavour):
+def fetch_real_name(app_dir, flavours):
     app_search = re.compile(r'.*<application.*').search
     name_search = re.compile(r'.*android:label="([^"]+)".*').search
     app_found = False
-    for f in manifest_paths(app_dir, flavour):
+    for f in manifest_paths(app_dir, flavours):
         if not has_extension(f, 'xml'):
             continue
         logging.debug("fetch_real_name: Checking manifest at " + f)
@@ -790,8 +878,8 @@ def fetch_real_name(app_dir, flavour):
 
 
 # Retrieve the version name
-def version_name(original, app_dir, flavour):
-    for f in manifest_paths(app_dir, flavour):
+def version_name(original, app_dir, flavours):
+    for f in manifest_paths(app_dir, flavours):
         if not has_extension(f, 'xml'):
             continue
         string = retrieve_string(app_dir, original)
@@ -813,7 +901,7 @@ def get_library_references(root_dir):
             relpath = os.path.join(root_dir, path)
             if not os.path.isdir(relpath):
                 continue
-            logging.info("Found subproject at %s" % path)
+            logging.debug("Found subproject at %s" % path)
             libraries.append(path)
     return libraries
 
@@ -831,11 +919,11 @@ def ant_subprojects(root_dir):
 
 def remove_debuggable_flags(root_dir):
     # Remove forced debuggable flags
-    logging.info("Removing debuggable flags")
+    logging.debug("Removing debuggable flags from %s" % root_dir)
     for root, dirs, files in os.walk(root_dir):
         if 'AndroidManifest.xml' in files:
             path = os.path.join(root, 'AndroidManifest.xml')
-            p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
+            p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path], output=False)
             if p.returncode != 0:
                 raise BuildException("Failed to remove debuggable flags of %s" % path)
 
@@ -917,7 +1005,8 @@ def parse_androidmanifests(paths, ignoreversions=None):
     return (max_version, max_vercode, max_package)
 
 
-class BuildException(Exception):
+class FDroidException(Exception):
+
     def __init__(self, value, detail=None):
         self.value = value
         self.detail = detail
@@ -939,12 +1028,12 @@ class BuildException(Exception):
         return ret
 
 
-class VCSException(Exception):
-    def __init__(self, value):
-        self.value = value
+class VCSException(FDroidException):
+    pass
 
-    def __str__(self):
-        return self.value
+
+class BuildException(FDroidException):
+    pass
 
 
 # Get the specified source library.
@@ -967,7 +1056,7 @@ def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
             name, subdir = name.split('/', 1)
 
     if name not in metadata.srclibs:
-        raise BuildException('srclib ' + name + ' not found.')
+        raise VCSException('srclib ' + name + ' not found.')
 
     srclib = metadata.srclibs[name]
 
@@ -1004,7 +1093,7 @@ def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
                     s_tuple = t
                     break
             if s_tuple is None:
-                raise BuildException('Missing recursive srclib %s for %s' % (
+                raise VCSException('Missing recursive srclib %s for %s' % (
                     lib, name))
             place_srclib(libdir, n, s_tuple[2])
             n += 1
@@ -1020,7 +1109,7 @@ def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
             p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
             if p.returncode != 0:
                 raise BuildException("Error running prepare command for srclib %s"
-                                     % name, p.stdout)
+                                     % name, p.output)
 
     if basepath:
         libdir = sdir
@@ -1072,7 +1161,7 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
         if p.returncode != 0:
             raise BuildException("Error running init command for %s:%s" %
-                                 (app['id'], build['version']), p.stdout)
+                                 (app['id'], build['version']), p.output)
 
     # Apply patches if any
     if build['patch']:
@@ -1106,13 +1195,15 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
     if build['subdir']:
         localprops += [os.path.join(root_dir, 'local.properties')]
     for path in localprops:
-        if not os.path.isfile(path):
-            continue
-        logging.info("Updating properties file at %s" % path)
-        f = open(path, 'r')
-        props = f.read()
-        f.close()
-        props += '\n'
+        props = ""
+        if os.path.isfile(path):
+            logging.info("Updating local.properties file at %s" % path)
+            f = open(path, 'r')
+            props += f.read()
+            f.close()
+            props += '\n'
+        else:
+            logging.info("Creating local.properties file at %s" % path)
         # Fix old-fashioned 'sdk-location' by copying
         # from sdk.dir, if necessary
         if build['oldsdkloc']:
@@ -1122,7 +1213,7 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         else:
             props += "sdk.dir=%s\n" % config['sdk_path']
             props += "sdk-location=%s\n" % config['sdk_path']
-        if 'ndk_path' in config:
+        if config['ndk_path']:
             # Add ndk location
             props += "ndk.dir=%s\n" % config['ndk_path']
             props += "ndk-location=%s\n" % config['ndk_path']
@@ -1133,11 +1224,9 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         f.write(props)
         f.close()
 
-    flavour = None
+    flavours = []
     if build['type'] == 'gradle':
-        flavour = build['gradle'].split('@')[0]
-        if flavour in ['main', 'yes', '']:
-            flavour = None
+        flavours = build['gradle']
 
         version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
         gradlepluginver = None
@@ -1149,13 +1238,6 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         if parent_dir.startswith(build_dir):
             gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
 
-        # Gradle execution dir build.gradle
-        if '@' in build['gradle']:
-            gradle_file = os.path.join(root_dir, build['gradle'].split('@', 1)[1], 'build.gradle')
-            gradle_file = os.path.normpath(gradle_file)
-            if gradle_file not in gradle_files:
-                gradle_files.append(gradle_file)
-
         for path in gradle_files:
             if gradlepluginver:
                 break
@@ -1178,15 +1260,7 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             n = build["target"].split('-')[1]
             FDroidPopen(['sed', '-i',
                          's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
-                         'build.gradle'],
-                        cwd=root_dir)
-            if '@' in build['gradle']:
-                gradle_dir = os.path.join(root_dir, build['gradle'].split('@', 1)[1])
-                gradle_dir = os.path.normpath(gradle_dir)
-                FDroidPopen(['sed', '-i',
-                             's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
-                             'build.gradle'],
-                            cwd=gradle_dir)
+                         'build.gradle'], cwd=root_dir, output=False)
 
     # Remove forced debuggable flags
     remove_debuggable_flags(root_dir)
@@ -1194,40 +1268,36 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
     # Insert version code and number into the manifest if necessary
     if build['forceversion']:
         logging.info("Changing the version name")
-        for path in manifest_paths(root_dir, flavour):
+        for path in manifest_paths(root_dir, flavours):
             if not os.path.isfile(path):
                 continue
             if has_extension(path, 'xml'):
-                p = SilentPopen(['sed', '-i',
-                                 's/android:versionName="[^"]*"/android:versionName="'
-                                 + build['version'] + '"/g',
-                                 path])
+                p = FDroidPopen(['sed', '-i',
+                                 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
+                                 path], output=False)
                 if p.returncode != 0:
                     raise BuildException("Failed to amend manifest")
             elif has_extension(path, 'gradle'):
-                p = SilentPopen(['sed', '-i',
-                                 's/versionName *=* *"[^"]*"/versionName = "'
-                                 + build['version'] + '"/g',
-                                 path])
+                p = FDroidPopen(['sed', '-i',
+                                 's/versionName *=* *"[^"]*"/versionName = "' + build['version'] + '"/g',
+                                 path], output=False)
                 if p.returncode != 0:
                     raise BuildException("Failed to amend build.gradle")
     if build['forcevercode']:
         logging.info("Changing the version code")
-        for path in manifest_paths(root_dir, flavour):
+        for path in manifest_paths(root_dir, flavours):
             if not os.path.isfile(path):
                 continue
             if has_extension(path, 'xml'):
-                p = SilentPopen(['sed', '-i',
-                                 's/android:versionCode="[^"]*"/android:versionCode="'
-                                 + build['vercode'] + '"/g',
-                                 path])
+                p = FDroidPopen(['sed', '-i',
+                                 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
+                                 path], output=False)
                 if p.returncode != 0:
                     raise BuildException("Failed to amend manifest")
             elif has_extension(path, 'gradle'):
-                p = SilentPopen(['sed', '-i',
-                                 's/versionCode *=* *[0-9]*/versionCode = '
-                                 + build['vercode'] + '/g',
-                                 path])
+                p = FDroidPopen(['sed', '-i',
+                                 's/versionCode *=* *[0-9]*/versionCode = ' + build['vercode'] + '/g',
+                                 path], output=False)
                 if p.returncode != 0:
                     raise BuildException("Failed to amend build.gradle")
 
@@ -1239,9 +1309,9 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             logging.info("Removing {0}".format(part))
             if os.path.lexists(dest):
                 if os.path.islink(dest):
-                    SilentPopen(['unlink ' + dest], shell=True)
+                    FDroidPopen(['unlink ' + dest], shell=True, output=False)
                 else:
-                    SilentPopen(['rm -rf ' + dest], shell=True)
+                    FDroidPopen(['rm -rf ' + dest], shell=True, output=False)
             else:
                 logging.info("...but it didn't exist")
 
@@ -1276,13 +1346,12 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
         if p.returncode != 0:
             raise BuildException("Error running prebuild command for %s:%s" %
-                                 (app['id'], build['version']), p.stdout)
+                                 (app['id'], build['version']), p.output)
 
     # Generate (or update) the ant build file, build.xml...
     if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
-        parms = [os.path.join(config['sdk_path'], 'tools', 'android'), 'update']
-        lparms = parms + ['lib-project']
-        parms = parms + ['project']
+        parms = ['android', 'update', 'lib-project']
+        lparms = ['android', 'update', 'project']
 
         if build['target']:
             parms += ['-t', build['target']]
@@ -1295,17 +1364,17 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         for d in update_dirs:
             subdir = os.path.join(root_dir, d)
             if d == '.':
-                print("Updating main project")
+                logging.debug("Updating main project")
                 cmd = parms + ['-p', d]
             else:
-                print("Updating subproject %s" % d)
+                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)
-            if p.returncode != 0 or p.stdout.startswith("Error: "):
-                raise BuildException("Failed to update project at %s" % d, p.stdout)
+            if p.returncode != 0 or p.output.startswith("Error: "):
+                raise BuildException("Failed to update project at %s" % d, p.output)
             # Clean update dirs via ant
             if d != '.':
                 logging.info("Cleaning subproject %s" % d)
@@ -1335,20 +1404,20 @@ def scan_source(build_dir, root_dir, thisbuild):
     usual_suspects = [
         re.compile(r'flurryagent', re.IGNORECASE),
         re.compile(r'paypal.*mpl', re.IGNORECASE),
-        re.compile(r'libgoogleanalytics', re.IGNORECASE),
+        re.compile(r'google.*analytics', re.IGNORECASE),
         re.compile(r'admob.*sdk.*android', re.IGNORECASE),
-        re.compile(r'googleadview', re.IGNORECASE),
-        re.compile(r'googleadmobadssdk', re.IGNORECASE),
+        re.compile(r'google.*ad.*view', re.IGNORECASE),
+        re.compile(r'google.*admob', re.IGNORECASE),
         re.compile(r'google.*play.*services', re.IGNORECASE),
         re.compile(r'crittercism', re.IGNORECASE),
         re.compile(r'heyzap', re.IGNORECASE),
         re.compile(r'jpct.*ae', re.IGNORECASE),
-        re.compile(r'youtubeandroidplayerapi', re.IGNORECASE),
+        re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
         re.compile(r'bugsense', re.IGNORECASE),
         re.compile(r'crashlytics', re.IGNORECASE),
         re.compile(r'ouya.*sdk', re.IGNORECASE),
         re.compile(r'libspen23', re.IGNORECASE),
-        ]
+    ]
 
     scanignore = getpaths(build_dir, thisbuild, 'scanignore')
     scandelete = getpaths(build_dir, thisbuild, 'scandelete')
@@ -1379,21 +1448,22 @@ def scan_source(build_dir, root_dir, thisbuild):
         logging.warn('Found %s at %s' % (what, fd))
 
     def handleproblem(what, fd, fp):
-        if todelete(fd):
+        if toignore(fd):
+            logging.info('Ignoring %s at %s' % (what, fd))
+        elif todelete(fd):
             removeproblem(what, fd, fp)
         else:
             logging.error('Found %s at %s' % (what, fd))
             return True
         return False
 
-    def insidedir(path, dirname):
-        return path.endswith('/%s' % dirname) or '/%s/' % dirname in path
-
     # Iterate through all files in the source code
-    for r, d, f in os.walk(build_dir):
+    for r, d, f in os.walk(build_dir, topdown=True):
 
-        if any(insidedir(r, d) for d in ('.hg', '.git', '.svn', '.bzr')):
-            continue
+        # It's topdown, so checking the basename is enough
+        for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
+            if ignoredir in d:
+                d.remove(ignoredir)
 
         for curfile in f:
 
@@ -1401,11 +1471,10 @@ def scan_source(build_dir, root_dir, thisbuild):
             fp = os.path.join(r, curfile)
             fd = fp[len(build_dir) + 1:]
 
-            # Check if this file has been explicitly excluded from scanning
-            if toignore(fd):
-                continue
-
-            mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
+            try:
+                mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
+            except UnicodeError:
+                warnproblem('malformed magic number', fd)
 
             if mime == 'application/x-sharedlib':
                 count += handleproblem('shared library', fd, fp)
@@ -1425,7 +1494,7 @@ def scan_source(build_dir, root_dir, thisbuild):
                     'application/java-archive',
                     'application/octet-stream',
                     'binary',
-                    ):
+            ):
 
                 if has_extension(fp, 'apk'):
                     removeproblem('APK file', fd, fp)
@@ -1531,19 +1600,19 @@ 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'],
+                      output=False)
     if p.returncode != 0:
         logging.critical("Failed to get apk manifest information")
         sys.exit(1)
-    for line in p.stdout.splitlines():
+    for line in p.output.splitlines():
         if 'android:debuggable' in line and not line.endswith('0x0'):
             return True
     return False
 
 
 class AsynchronousFileReader(threading.Thread):
+
     '''
     Helper class to implement asynchronous reading of a file
     in a separate thread. Pushes read lines on a queue to
@@ -1569,11 +1638,15 @@ class AsynchronousFileReader(threading.Thread):
 
 class PopenResult:
     returncode = None
-    stdout = ''
+    output = ''
 
 
-def SilentPopen(commands, cwd=None, shell=False):
-    return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
+def SdkToolsPopen(commands, cwd=None, shell=False, output=True):
+    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=output)
 
 
 def FDroidPopen(commands, cwd=None, shell=False, output=True):
@@ -1585,14 +1658,20 @@ def FDroidPopen(commands, cwd=None, shell=False, output=True):
     :returns: A PopenResult.
     """
 
+    global env
+
     if cwd:
         cwd = os.path.normpath(cwd)
         logging.debug("Directory: %s" % cwd)
     logging.debug("> %s" % ' '.join(commands))
 
     result = PopenResult()
-    p = subprocess.Popen(commands, cwd=cwd, shell=shell,
-                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    p = None
+    try:
+        p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
+                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    except OSError, e:
+        raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
 
     stdout_queue = Queue.Queue()
     stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
@@ -1602,16 +1681,15 @@ def FDroidPopen(commands, cwd=None, shell=False, output=True):
     while not stdout_reader.eof():
         while not stdout_queue.empty():
             line = stdout_queue.get()
-            if output:
+            if output and options.verbose:
                 # Output directly to console
-                sys.stdout.write(line)
-                sys.stdout.flush()
-            result.stdout += line
+                sys.stderr.write(line)
+                sys.stderr.flush()
+            result.output += line
 
         time.sleep(0.1)
 
-    p.communicate()
-    result.returncode = p.returncode
+    result.returncode = p.wait()
     return result
 
 
@@ -1622,8 +1700,9 @@ def remove_signing_keys(build_dir):
         re.compile(r'^[\t ]*signingConfig [^ ]*$'),
         re.compile(r'.*android\.signingConfigs\.[^{]*$'),
         re.compile(r'.*variant\.outputFile = .*'),
+        re.compile(r'.*output\.outputFile = .*'),
         re.compile(r'.*\.readLine\(.*'),
-        ]
+    ]
     for root, dirs, files in os.walk(build_dir):
         if 'build.gradle' in files:
             path = os.path.join(root, 'build.gradle')
@@ -1664,7 +1743,7 @@ def remove_signing_keys(build_dir):
                 'build.properties',
                 'default.properties',
                 'ant.properties',
-                ]:
+        ]:
             if propfile in files:
                 path = os.path.join(root, propfile)
 
@@ -1713,3 +1792,37 @@ def place_srclib(root_dir, number, libpath):
                 o.write(line)
         if not placed:
             o.write('android.library.reference.%d=%s\n' % (number, relpath))
+
+
+def compare_apks(apk1, apk2, tmp_dir):
+    """Compare two apks
+
+    Returns None if the apk content is the same (apart from the signing key),
+    otherwise a string describing what's different, or what went wrong when
+    trying to do the comparison.
+    """
+
+    thisdir = os.path.join(tmp_dir, 'this_apk')
+    thatdir = os.path.join(tmp_dir, 'that_apk')
+    for d in [thisdir, thatdir]:
+        if os.path.exists(d):
+            shutil.rmtree(d)
+        os.mkdir(d)
+
+    if subprocess.call(['jar', 'xf',
+                        os.path.abspath(apk1)],
+                       cwd=thisdir) != 0:
+        return("Failed to unpack " + apk1)
+    if subprocess.call(['jar', 'xf',
+                        os.path.abspath(apk2)],
+                       cwd=thatdir) != 0:
+        return("Failed to unpack " + apk2)
+
+    p = FDroidPopen(['diff', '-r', 'this_apk', 'that_apk'], cwd=tmp_dir,
+                    output=False)
+    lines = p.output.splitlines()
+    if len(lines) != 1 or 'META-INF' not in lines[0]:
+        return("Unexpected diff output - " + p.output)
+
+    # If we get here, it seems like they're the same!
+    return None