chiark / gitweb /
Apply some autopep8-python2 suggestions
[fdroidserver.git] / fdroidserver / common.py
index ba8080a70778ed7cf6668079f95b12363113c914..b9e7b81c615926c69f73fc45ae2bfe30228ac3e6 100644 (file)
@@ -42,11 +42,13 @@ env = None
 default_config = {
     'sdk_path': "$ANDROID_HOME",
     'ndk_path': "$ANDROID_NDK",
-    'build_tools': "20.0.0",
+    '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,
@@ -74,20 +76,20 @@ default_config = {
 }
 
 
-def fill_config_defaults(config):
+def fill_config_defaults(thisconfig):
     for k, v in default_config.items():
-        if k not in config:
-            config[k] = v
+        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 = config[k]
+        v = thisconfig[k]
         orig = v
         v = os.path.expanduser(v)
         v = os.path.expandvars(v)
         if orig != v:
-            config[k] = v
-            config[k + '_orig'] = orig
+            thisconfig[k] = v
+            thisconfig[k + '_orig'] = orig
 
 
 def read_config(opts, config_file='config.py'):
@@ -128,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
@@ -195,36 +165,73 @@ def read_config(opts, config_file='config.py'):
     return config
 
 
-def test_sdk_exists(config):
-    if config['sdk_path'] == default_config['sdk_path']:
+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.exists(config['sdk_path']):
-        logging.critical('Android SDK path "' + config['sdk_path'] + '" does not exist!')
+    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(config['sdk_path']):
-        logging.critical('Android SDK path "' + config['sdk_path'] + '" is not a directory!')
+    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(config['sdk_path'], d)):
+        if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
             logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
-                config['sdk_path'], d))
+                thisconfig['sdk_path'], d))
             return False
     return True
 
 
-def test_build_tools_exists(config):
-    if not test_sdk_exists(config):
-        return False
-    build_tools = os.path.join(config['sdk_path'], 'build-tools')
-    versioned_build_tools = os.path.join(build_tools, config['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
-    return True
+        sys.exit(3)
 
 
 def write_password_file(pwtype, password=None):
@@ -384,6 +391,7 @@ def getsrclibvcs(name):
 
 
 class vcs:
+
     def __init__(self, remote, local):
 
         # svn, git-svn and bzr may require auth
@@ -432,9 +440,8 @@ class vcs:
                     writeback = False
                 else:
                     deleterepo = True
-                    logging.info(
-                        "Repository details for %s changed - deleting" % (
-                            self.local))
+                    logging.info("Repository details for %s changed - deleting" % (
+                        self.local))
             else:
                 deleterepo = True
                 logging.info("Repository details for %s missing - deleting" % (
@@ -494,7 +501,7 @@ 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)
+        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')
@@ -510,12 +517,12 @@ class vcs_git(vcs):
         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", 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", p.output)
             if not self.refreshed:
@@ -523,28 +530,28 @@ class vcs_git(vcs):
                 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
                 if p.returncode != 0:
                     raise VCSException("Git fetch failed", p.output)
-                p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
+                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 = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
+                p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
                 if p.returncode != 0:
                     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 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
+                    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
         # 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 = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
+        p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
         if p.returncode != 0:
             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", p.output)
 
@@ -567,10 +574,10 @@ 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.output)
-        p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
+        p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
         if p.returncode != 0:
             raise VCSException("Git submodule sync failed", p.output)
         p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
@@ -579,15 +586,16 @@ class vcs_git(vcs):
 
     def gettags(self):
         self.checkrepo()
-        p = SilentPopen(['git', 'tag'], cwd=self.local)
+        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)
+        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:]
 
 
@@ -608,7 +616,7 @@ 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)
+        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')
@@ -626,12 +634,12 @@ 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:
                     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:
                     self.clone_failed = True
                     raise VCSException("Git svn clone failed", p.output)
@@ -639,20 +647,20 @@ class vcs_gitsvn(vcs):
         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", 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", 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", p.output)
                 self.refreshed = True
@@ -662,8 +670,7 @@ class vcs_gitsvn(vcs):
             nospaces_rev = rev.replace(' ', '%20')
             # Try finding a svn tag
             for treeish in ['origin/', '']:
-                p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
-                                cwd=self.local)
+                p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
                 if p.returncode == 0:
                     break
             if p.returncode != 0:
@@ -684,8 +691,7 @@ class vcs_gitsvn(vcs):
 
                     svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
 
-                    p = SilentPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish],
-                                    cwd=self.local)
+                    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:
@@ -693,17 +699,17 @@ class vcs_gitsvn(vcs):
 
                 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 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 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", p.output)
 
@@ -716,7 +722,7 @@ class vcs_gitsvn(vcs):
 
     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.output.strip()
@@ -729,16 +735,16 @@ 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:
                 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", 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", p.output)
                 self.refreshed = True
@@ -746,22 +752,22 @@ class vcs_hg(vcs):
         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 of '%s' failed" % rev, p.output)
-        p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
+        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.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", p.output)
         elif p.returncode != 0:
             raise VCSException("HG purge failed", p.output)
 
     def gettags(self):
-        p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
+        p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
         return p.output.splitlines()[1:]
 
 
@@ -772,27 +778,27 @@ 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:
                 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", 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", 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 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.output.splitlines()]
 
@@ -838,6 +844,8 @@ def manifest_paths(app_dir, flavours):
          os.path.join(app_dir, 'build.gradle')]
 
     for flavour in flavours:
+        if flavour == 'yes':
+            continue
         possible_manifests.append(
             os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
 
@@ -915,7 +923,7 @@ def remove_debuggable_flags(root_dir):
     for root, dirs, files in os.walk(root_dir):
         if 'AndroidManifest.xml' in files:
             path = os.path.join(root, 'AndroidManifest.xml')
-            p = SilentPopen(['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)
 
@@ -998,6 +1006,7 @@ def parse_androidmanifests(paths, ignoreversions=None):
 
 
 class FDroidException(Exception):
+
     def __init__(self, value, detail=None):
         self.value = value
         self.detail = detail
@@ -1186,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']:
@@ -1202,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']
@@ -1247,10 +1258,9 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
 
         if build['target']:
             n = build["target"].split('-')[1]
-            SilentPopen(['sed', '-i',
+            FDroidPopen(['sed', '-i',
                          's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
-                         'build.gradle'],
-                        cwd=root_dir)
+                         'build.gradle'], cwd=root_dir, output=False)
 
     # Remove forced debuggable flags
     remove_debuggable_flags(root_dir)
@@ -1262,17 +1272,15 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             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']:
@@ -1281,17 +1289,15 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             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")
 
@@ -1303,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")
 
@@ -1344,8 +1350,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']]
@@ -1363,7 +1369,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)
@@ -1411,7 +1417,7 @@ def scan_source(build_dir, root_dir, thisbuild):
         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')
@@ -1488,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)
@@ -1594,9 +1600,8 @@ 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)
@@ -1607,6 +1612,7 @@ def isApkDebuggable(apkfile, config):
 
 
 class AsynchronousFileReader(threading.Thread):
+
     '''
     Helper class to implement asynchronous reading of a file
     in a separate thread. Pushes read lines on a queue to
@@ -1635,8 +1641,12 @@ class PopenResult:
     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):
@@ -1690,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')
@@ -1732,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)
 
@@ -1781,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