chiark / gitweb /
Switch all headers to python3
[fdroidserver.git] / fdroidserver / publish.py
index 8fecb6ec024e20a073b80ed56a58e9a555c61970..c34382342f806e9e5a30edc4b01753e53dc171c0 100644 (file)
@@ -1,5 +1,4 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
 #
 # publish.py - part of the FDroid server tools
 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
@@ -23,12 +22,12 @@ import os
 import shutil
 import md5
 import glob
-from optparse import OptionParser
+from argparse import ArgumentParser
 import logging
 
 import common
 import metadata
-from common import FDroidPopen, BuildException
+from common import FDroidPopen, SdkToolsPopen, BuildException
 
 config = None
 options = None
@@ -39,15 +38,18 @@ def main():
     global config, options
 
     # Parse command line...
-    parser = OptionParser(usage="Usage: %prog [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
-    parser.add_option("-v", "--verbose", action="store_true", default=False,
-                      help="Spew out even more information than normal")
-    parser.add_option("-q", "--quiet", action="store_true", default=False,
-                      help="Restrict output to warnings and errors")
-    (options, args) = parser.parse_args()
+    parser = ArgumentParser(usage="%(prog)s [options] "
+                            "[APPID[:VERCODE] [APPID[:VERCODE] ...]]")
+    common.setup_global_opts(parser)
+    parser.add_argument("appid", nargs='*', help="app-id with optional versioncode in the form APPID[:VERCODE]")
+    options = parser.parse_args()
 
     config = common.read_config(options)
 
+    if not ('jarsigner' in config and 'keytool' in config):
+        logging.critical('Java JDK not found! Install in standard location or set java_paths!')
+        sys.exit(1)
+
     log_dir = 'logs'
     if not os.path.isdir(log_dir):
         logging.info("Creating log directory")
@@ -68,12 +70,15 @@ def main():
         logging.warning("No unsigned directory - nothing to do")
         sys.exit(1)
 
-    for f in [config['keystorepassfile'], config['keystore'], config['keypassfile']]:
+    for f in [config['keystorepassfile'],
+              config['keystore'],
+              config['keypassfile']]:
         if not os.path.exists(f):
             logging.error("Config error - missing '{0}'".format(f))
             sys.exit(1)
 
-    # It was suggested at https://dev.guardianproject.info/projects/bazaar/wiki/FDroid_Audit
+    # It was suggested at
+    #    https://dev.guardianproject.info/projects/bazaar/wiki/FDroid_Audit
     # that a package could be crafted, such that it would use the same signing
     # key as an existing app. While it may be theoretically possible for such a
     # colliding package ID to be generated, it seems virtually impossible that
@@ -82,17 +87,18 @@ def main():
     # Nonetheless, to be sure, before publishing we check that there are no
     # collisions, and refuse to do any publishing if that's the case...
     allapps = metadata.read_metadata()
-    vercodes = common.read_pkg_args(args, True)
+    vercodes = common.read_pkg_args(options.appid, True)
     allaliases = []
-    for app in allapps:
+    for appid in allapps:
         m = md5.new()
-        m.update(app['id'])
+        m.update(appid)
         keyalias = m.hexdigest()[:8]
         if keyalias in allaliases:
             logging.error("There is a keyalias collision - publishing halted")
             sys.exit(1)
         allaliases.append(keyalias)
-    logging.info("{0} apps, {0} key aliases".format(len(allapps), len(allaliases)))
+    logging.info("{0} apps, {0} key aliases".format(len(allapps),
+                                                    len(allaliases)))
 
     # Process any apks that are waiting to be signed...
     for apkfile in sorted(glob.glob(os.path.join(unsigned_dir, '*.apk'))):
@@ -106,61 +112,93 @@ def main():
                 continue
         logging.info("Processing " + apkfile)
 
-        # Figure out the key alias name we'll use. Only the first 8
-        # characters are significant, so we'll use the first 8 from
-        # the MD5 of the app's ID and hope there are no collisions.
-        # If a collision does occur later, we're going to have to
-        # come up with a new alogrithm, AND rename all existing keys
-        # in the keystore!
-        if appid in config['keyaliases']:
-            # For this particular app, the key alias is overridden...
-            keyalias = config['keyaliases'][appid]
-            if keyalias.startswith('@'):
+        # There ought to be valid metadata for this app, otherwise why are we
+        # trying to publish it?
+        if appid not in allapps:
+            logging.error("Unexpected {0} found in unsigned directory"
+                          .format(apkfilename))
+            sys.exit(1)
+        app = allapps[appid]
+
+        if app.Binaries is not None:
+
+            # It's an app where we build from source, and verify the apk
+            # contents against a developer's binary, and then publish their
+            # version if everything checks out.
+            # The binary should already have been retrieved during the build
+            # process.
+            srcapk = apkfile + ".binary"
+
+            # Compare our unsigned one with the downloaded one...
+            compare_result = common.verify_apks(srcapk, apkfile, tmp_dir)
+            if compare_result:
+                logging.error("...verification failed - publish skipped : "
+                              + compare_result)
+                continue
+
+            # Success! So move the downloaded file to the repo, and remove
+            # our built version.
+            shutil.move(srcapk, os.path.join(output_dir, apkfilename))
+            os.remove(apkfile)
+
+        else:
+
+            # It's a 'normal' app, i.e. we sign and publish it...
+
+            # Figure out the key alias name we'll use. Only the first 8
+            # characters are significant, so we'll use the first 8 from
+            # the MD5 of the app's ID and hope there are no collisions.
+            # If a collision does occur later, we're going to have to
+            # come up with a new alogrithm, AND rename all existing keys
+            # in the keystore!
+            if appid in config['keyaliases']:
+                # For this particular app, the key alias is overridden...
+                keyalias = config['keyaliases'][appid]
+                if keyalias.startswith('@'):
+                    m = md5.new()
+                    m.update(keyalias[1:])
+                    keyalias = m.hexdigest()[:8]
+            else:
                 m = md5.new()
-                m.update(keyalias[1:])
+                m.update(appid)
                 keyalias = m.hexdigest()[:8]
-        else:
-            m = md5.new()
-            m.update(appid)
-            keyalias = m.hexdigest()[:8]
-        logging.info("Key alias: " + keyalias)
-
-        # See if we already have a key for this application, and
-        # if not generate one...
-        p = FDroidPopen(['keytool', '-list',
-                         '-alias', keyalias, '-keystore', config['keystore'],
-                         '-storepass:file', config['keystorepassfile']])
-        if p.returncode != 0:
-            logging.info("Key does not exist - generating...")
-            p = FDroidPopen(['keytool', '-genkey',
-                             '-keystore', config['keystore'],
-                             '-alias', keyalias,
-                             '-keyalg', 'RSA', '-keysize', '2048',
-                             '-validity', '10000',
+            logging.info("Key alias: " + keyalias)
+
+            # See if we already have a key for this application, and
+            # if not generate one...
+            p = FDroidPopen([config['keytool'], '-list',
+                             '-alias', keyalias, '-keystore', config['keystore'],
+                             '-storepass:file', config['keystorepassfile']])
+            if p.returncode != 0:
+                logging.info("Key does not exist - generating...")
+                p = FDroidPopen([config['keytool'], '-genkey',
+                                 '-keystore', config['keystore'],
+                                 '-alias', keyalias,
+                                 '-keyalg', 'RSA', '-keysize', '2048',
+                                 '-validity', '10000',
+                                 '-storepass:file', config['keystorepassfile'],
+                                 '-keypass:file', config['keypassfile'],
+                                 '-dname', config['keydname']])
+                # TODO keypass should be sent via stdin
+                if p.returncode != 0:
+                    raise BuildException("Failed to generate key")
+
+            # Sign the application...
+            p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
                              '-storepass:file', config['keystorepassfile'],
-                             '-keypass:file', config['keypassfile'],
-                             '-dname', config['keydname']])
+                             '-keypass:file', config['keypassfile'], '-sigalg',
+                             'SHA1withRSA', '-digestalg', 'SHA1',
+                             apkfile, keyalias])
             # TODO keypass should be sent via stdin
             if p.returncode != 0:
-                raise BuildException("Failed to generate key")
-
-        # Sign the application...
-        p = FDroidPopen(['jarsigner', '-keystore', config['keystore'],
-                         '-storepass:file', config['keystorepassfile'],
-                         '-keypass:file', config['keypassfile'], '-sigalg',
-                         'MD5withRSA', '-digestalg', 'SHA1',
-                         apkfile, keyalias])
-        # TODO keypass should be sent via stdin
-        if p.returncode != 0:
-            raise BuildException("Failed to sign application")
-
-        # Zipalign it...
-        p = FDroidPopen([os.path.join(config['sdk_path'], 'tools', 'zipalign'),
-                         '-v', '4', apkfile,
-                         os.path.join(output_dir, apkfilename)])
-        if p.returncode != 0:
-            raise BuildException("Failed to align application")
-        os.remove(apkfile)
+                raise BuildException("Failed to sign application")
+
+            # Zipalign it...
+            p = SdkToolsPopen(['zipalign', '-v', '4', apkfile,
+                               os.path.join(output_dir, apkfilename)])
+            if p.returncode != 0:
+                raise BuildException("Failed to align application")
+            os.remove(apkfile)
 
         # Move the source tarball into the output directory...
         tarfilename = apkfilename[:-4] + '_src.tar.gz'