chiark / gitweb /
Create archive repo (configurable option)
authorCiaran Gultnieks <ciaran@ciarang.com>
Thu, 9 May 2013 19:09:17 +0000 (20:09 +0100)
committerCiaran Gultnieks <ciaran@ciarang.com>
Thu, 9 May 2013 19:09:17 +0000 (20:09 +0100)
config.sample.py
fdroidserver/build.py
fdroidserver/server.py
fdroidserver/update.py

index 2d69efc5e4904b56566f267b4caf72adae613d64..f8cedc23bd6717af49593d7873503e14ddadc5c7 100644 (file)
@@ -21,6 +21,20 @@ are binaries built from source by the admin of f-droid.org using the tools on
 https://gitorious.org/f-droid.
 """
 
+# As above, but for the archive repo.
+# archive_older sets the number of versions kept in the main repo, with all
+# older ones going to the archive. Set it to 0, and there will be no archive
+# repository, and no need to define the other archive_ values.
+archive_older = 3
+archive_url = "https://f-droid.org/archive"
+archive_name = "F-Droid Archive"
+archive_icon = "fdroid-icon.png"
+archive_description = """
+The archive repository of the F-Droid client. This contains older versions
+of applications from the main repository.
+"""
+
+
 #The key (from the keystore defined below) to be used for signing the
 #repository itself. Can be None for an unsigned repository.
 repo_keyalias = None
index 96b9e9a40a47d77f07689cf1dc49a88c6a50b7b2..326fabb7a277a599a7aee8a1342884d788490581 100644 (file)
@@ -22,10 +22,8 @@ import os
 import shutil
 import subprocess
 import re
-import zipfile
 import tarfile
 import traceback
-from xml.dom.minidom import Document
 from optparse import OptionParser
 
 import common
@@ -465,8 +463,8 @@ def build_local(app, thisbuild, vcs, build_dir, output_dir, extlib_dir, tmp_dir,
             os.path.join(output_dir, tarfilename))
 
 
-def trybuild(app, thisbuild, build_dir, output_dir, extlib_dir, tmp_dir,
-        repo_dir, vcs, test, server, install, force, verbose=False):
+def trybuild(app, thisbuild, build_dir, output_dir, also_check_dir, extlib_dir,
+        tmp_dir, repo_dir, vcs, test, server, install, force, verbose=False):
     """
     Build a particular version of an application, if it needs building.
 
@@ -481,6 +479,12 @@ def trybuild(app, thisbuild, build_dir, output_dir, extlib_dir, tmp_dir,
     if os.path.exists(dest) or (not test and os.path.exists(dest_repo)):
         return False
 
+    if also_check_dir:
+        dest_also = os.path.join(also_check_dir, app['id'] + '_' +
+                thisbuild['vercode'] + '.apk')
+        if os.path.exists(dest_also):
+            return False
+
     if thisbuild['commit'].startswith('!'):
         return False
 
@@ -554,10 +558,13 @@ options = None
 def main():
 
     global options
+
     # Read configuration...
     globals()['build_server_always'] = False
     globals()['mvn3'] = "mvn3"
+    globals()['archive_older'] = 0
     execfile('config.py', globals())
+
     options, args = parse_commandline()
     if build_server_always:
         options.server = True
@@ -586,6 +593,11 @@ def main():
             print "Creating output directory"
             os.makedirs(output_dir)
 
+    if archive_older != 0:
+        also_check_dir = 'archive'
+    else:
+        also_check_dir = None
+
     repo_dir = 'repo'
 
     build_dir = 'build'
@@ -628,9 +640,10 @@ def main():
         for thisbuild in app['builds']:
             wikilog = None
             try:
-                if trybuild(app, thisbuild, build_dir, output_dir, extlib_dir,
-                        tmp_dir, repo_dir, vcs, options.test, options.server,
-                        options.install, options.force, options.verbose):
+                if trybuild(app, thisbuild, build_dir, output_dir, also_check_dir,
+                        extlib_dir, tmp_dir, repo_dir, vcs, options.test,
+                        options.server, options.install, options.force,
+                        options.verbose):
                     build_succeeded.append(app)
                     wikilog = "Build succeeded"
             except BuildException as be:
index e97539899bc15d6a15cd251d707350221a687eea..ee239912602c7d8fe2b1ee8d64ffbb12e71233b3 100644 (file)
@@ -25,6 +25,8 @@ from optparse import OptionParser
 def main():
 
     #Read configuration...
+    global archive_older
+    archive_older = 0
     execfile('config.py', globals())
 
     # Parse command line...
@@ -41,12 +43,22 @@ def main():
         print "The only command currently supported is 'update'"
         sys.exit(1)
 
-    if subprocess.call(['rsync', '-u', '-v', '-r', '--delete', '--exclude', 'repo/index.xml', '--exclude', 'repo/index.jar', 'repo', serverwebroot]) != 0:
-        sys.exit(1)
-    if subprocess.call(['rsync', '-u', '-v', '-r', '--delete', 'repo/index.xml', serverwebroot + '/repo']) != 0:
-        sys.exit(1)
-    if subprocess.call(['rsync', '-u', '-v', '-r', '--delete', 'repo/index.jar', serverwebroot + '/repo']) != 0:
-        sys.exit(1)
+    repodirs = ['repo']
+    if archive_older != 0:
+        repodirs.append('archive')
+
+    for repodir in repodirs:
+        index = os.path.join(repodir, 'index.xml')
+        indexjar = os.path.join(repodir, 'index.jar')
+        if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
+                '--exclude', index, '--exclude', indexjar, repodir, serverwebroot]) != 0:
+            sys.exit(1)
+        if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
+                index, serverwebroot + '/' + repodir]) != 0:
+            sys.exit(1)
+        if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
+                indexjar, serverwebroot + '/' + repodir]) != 0:
+            sys.exit(1)
 
     sys.exit(0)
 
index 750d8f1899cc724c47c55ab35b047f71fcfe81a0..f4793a666d24159af58922223277c71e20b861c9 100644 (file)
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 #
 # update.py - part of the FDroid server tools
-# Copyright (C) 2010-12, Ciaran Gultnieks, ciaran@ciarang.com
+# Copyright (C) 2010-2013, Ciaran Gultnieks, ciaran@ciarang.com
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published by
@@ -182,88 +182,52 @@ def update_wiki(apps, apks, verbose=False):
                 print "...FAILED to create page"
 
 
+def delete_disabled_builds(apps, apkcache, repodirs):
+    """Delete disabled build outputs.
 
-def main():
+    :param apps: list of all applications, as per common.read_metadata
+    :param apkcache: current apk cache information
+    :param repodirs: the repo directories to process
+    """
+    for app in apps:
+        for build in app['builds']:
+            if build['commit'].startswith('!'):
+                apkfilename = app['id'] + '_' + str(build['vercode']) + '.apk'
+                for repodir in repodirs:
+                    apkpath = os.path.join(repodir, apkfilename)
+                    srcpath = os.path.join(repodir, apkfilename[:-4] + "_src.tar.gz")
+                    for name in [apkpath, srcpath]:
+                        if os.path.exists(name):
+                            print "Deleting disabled build output " + apkfilename
+                            os.remove(name)
+                if apkfilename in apkcache:
+                    del apkcache[apkfilename]
 
-    # Read configuration...
-    global update_stats
-    update_stats = False
-    execfile('config.py', globals())
 
-    # Parse command line...
-    parser = OptionParser()
-    parser.add_option("-c", "--createmeta", action="store_true", default=False,
-                      help="Create skeleton metadata files that are missing")
-    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="No output, except for warnings and errors")
-    parser.add_option("-b", "--buildreport", action="store_true", default=False,
-                      help="Report on build data status")
-    parser.add_option("-i", "--interactive", default=False, action="store_true",
-                      help="Interactively ask about things that need updating.")
-    parser.add_option("-e", "--editor", default="/etc/alternatives/editor",
-                      help="Specify editor to use in interactive mode. Default "+
-                          "is /etc/alternatives/editor")
-    parser.add_option("-w", "--wiki", default=False, action="store_true",
-                      help="Update the wiki")
-    parser.add_option("", "--pretty", action="store_true", default=False,
-                      help="Produce human-readable index.xml")
-    parser.add_option("--clean", action="store_true", default=False,
-                      help="Clean update - don't uses caches, reprocess all apks")
-    (options, args) = parser.parse_args()
+def scan_apks(apps, apkcache, repodir, knownapks):
+    """Scan the apks in the given repo directory.
 
+    This also extracts the icons.
 
-    icon_dir=os.path.join('repo','icons')
+    :param apps: list of all applications, as per common.read_metadata
+    :param apkcache: current apk cache information
+    :param repodir: repo directory to scan
+    :param knownapks: known apks info
+    :returns: (apks, cachechanged) where apks is a list of apk information,
+              and cachechanged is True if the apkcache got changed.
+    """
 
+    cachechanged = False
+
+    icon_dir = os.path.join(repodir ,'icons')
     # Delete and re-create the icon directory...
     if options.clean and os.path.exists(icon_dir):
         shutil.rmtree(icon_dir)
     if not os.path.exists(icon_dir):
         os.makedirs(icon_dir)
 
-    warnings = 0
-
-    # Get all apps...
-    apps = common.read_metadata(verbose=options.verbose)
-
-    # Generate a list of categories...
-    categories = []
-    for app in apps:
-        cats = app['Category'].split(';')
-        for cat in cats:
-            if cat not in categories:
-                categories.append(cat)
-
-    # Read known apks data (will be updated and written back when we've finished)
-    knownapks = common.KnownApks()
-
-    # Gather information about all the apk files in the repo directory, using
-    # cached data if possible.
-    apkcachefile = os.path.join('tmp', 'apkcache')
-    if not options.clean and os.path.exists(apkcachefile):
-        with open(apkcachefile, 'rb') as cf:
-            apkcache = pickle.load(cf)
-    else:
-        apkcache = {}
-    cachechanged = False
-
-    # Check repo directory for disabled builds and remove them...
-    for app in apps:
-        for build in app['builds']:
-            if build['commit'].startswith('!'):
-                apkfilename = app['id'] + '_' + str(build['vercode']) + '.apk'
-                apkpath = os.path.join('repo', apkfilename)
-                srcpath = apkfilename[:-4] + "_src.tar.gz"
-                for name in [apkpath, srcpath]:
-                    if os.path.exists(name):
-                        print "Deleting disabled build output " + apkfilename
-                        os.remove(name)
-                if apkfilename in apkcache:
-                    del apkcache[apkfilename]
-
     apks = []
-    for apkfile in glob.glob(os.path.join('repo','*.apk')):
+    for apkfile in glob.glob(os.path.join(repodir, '*.apk')):
 
         apkfilename = apkfile[5:]
         if apkfilename.find(' ') != -1:
@@ -282,7 +246,7 @@ def main():
                 print "Processing " + apkfilename
             thisinfo = {}
             thisinfo['apkname'] = apkfilename
-            if os.path.exists(os.path.join('repo', srcfilename)):
+            if os.path.exists(os.path.join(repodir, srcfilename)):
                 thisinfo['srcname'] = srcfilename
             thisinfo['size'] = os.path.getsize(apkfile)
             thisinfo['permissions'] = []
@@ -377,7 +341,6 @@ def main():
                 iconfile.close()
             except:
                 print "WARNING: Error retrieving icon file"
-                warnings += 1
             apk.close()
 
             # Record in known apks, getting the added date at the same time..
@@ -390,79 +353,20 @@ def main():
 
         apks.append(thisinfo)
 
-    if cachechanged:
-        with open(apkcachefile, 'wb') as cf:
-            pickle.dump(apkcache, cf)
+    return apks, cachechanged
 
-    # Some information from the apks needs to be applied up to the application
-    # level. When doing this, we use the info from the most recent version's apk.
-    # We deal with figuring out when the app was added and last updated at the
-    # same time.
-    for app in apps:
-        bestver = 0
-        added = None
-        lastupdated = None
-        for apk in apks:
-            if apk['id'] == app['id']:
-                if apk['versioncode'] > bestver:
-                    bestver = apk['versioncode']
-                    bestapk = apk
 
-                if 'added' in apk:
-                    if not added or apk['added'] < added:
-                        added = apk['added']
-                    if not lastupdated or apk['added'] > lastupdated:
-                        lastupdated = apk['added']
-
-        if added:
-            app['added'] = added
-        else:
-            print "WARNING: Don't know when " + app['id'] + " was added"
-        if lastupdated:
-            app['lastupdated'] = lastupdated
-        else:
-            print "WARNING: Don't know when " + app['id'] + " was last updated"
+def make_index(apps, apks, repodir, archive, categories):
+    """Make a repo index.
 
-        if bestver == 0:
-            if app['Name'] is None:
-                app['Name'] = app['id']
-            app['icon'] = ''
-            if app['Disabled'] is None:
-                print "WARNING: Application " + app['id'] + " has no packages"
-        else:
-            if app['Name'] is None:
-                app['Name'] = bestapk['name']
-            app['icon'] = bestapk['icon']
+    :param apps: fully populated apps list
+    :param apks: full populated apks list
+    :param repodir: the repo directory
+    :param archive: True if this is the archive repo, False if it's the
+                    main one.
+    :param categories: list of categories
+    """
 
-    # Generate warnings for apk's with no metadata (or create skeleton
-    # metadata files, if requested on the command line)
-    for apk in apks:
-        found = False
-        for app in apps:
-            if app['id'] == apk['id']:
-                found = True
-                break
-        if not found:
-            if options.createmeta:
-                f = open(os.path.join('metadata', apk['id'] + '.txt'), 'w')
-                f.write("License:Unknown\n")
-                f.write("Web Site:\n")
-                f.write("Source Code:\n")
-                f.write("Issue Tracker:\n")
-                f.write("Summary:" + apk['name'] + "\n")
-                f.write("Description:\n")
-                f.write(apk['name'] + "\n")
-                f.write(".\n")
-                f.close()
-                print "Generated skeleton metadata for " + apk['id']
-            else:
-                print "WARNING: " + apk['apkname'] + " (" + apk['id'] + ") has no metadata"
-                print "       " + apk['name'] + " - " + apk['version']  
-
-    #Sort the app list by name, then the web site doesn't have to by default:
-    apps = sorted(apps, key=lambda app: app['Name'].upper())
-
-    # Create the index
     doc = Document()
 
     def addElement(name, value, doc, parent):
@@ -478,9 +382,16 @@ def main():
     doc.appendChild(root)
 
     repoel = doc.createElement("repo")
-    repoel.setAttribute("name", repo_name)
-    repoel.setAttribute("icon", os.path.basename(repo_icon))
-    repoel.setAttribute("url", repo_url)
+    if archive:
+        repoel.setAttribute("name", archive_name)
+        repoel.setAttribute("icon", os.path.basename(archive_icon))
+        repoel.setAttribute("url", archive_url)
+        addElement('description', archive_description, doc, repoel)
+    else:
+        repoel.setAttribute("name", repo_name)
+        repoel.setAttribute("icon", os.path.basename(repo_icon))
+        repoel.setAttribute("url", repo_url)
+        addElement('description', repo_description, doc, repoel)
 
     if repo_keyalias != None:
 
@@ -509,30 +420,19 @@ def main():
 
         repoel.setAttribute("pubkey", extract_pubkey())
 
-    addElement('description', repo_description, doc, repoel)
     root.appendChild(repoel)
 
-    apps_inrepo = 0
-    apps_disabled = 0
-    apps_nopkg = 0
-
     for app in apps:
 
         if app['Disabled'] is None:
 
             # Get a list of the apks for this app...
-            gotcurrentver = False
             apklist = []
             for apk in apks:
                 if apk['id'] == app['id']:
-                    if str(apk['versioncode']) == app['Current Version Code']:
-                        gotcurrentver = True
                     apklist.append(apk)
 
-            if len(apklist) == 0:
-                apps_nopkg += 1
-            else:
-                apps_inrepo += 1
+            if len(apklist) != 0:
                 apel = doc.createElement("application")
                 apel.setAttribute("id", app['id'])
                 root.appendChild(apel)
@@ -627,62 +527,7 @@ def main():
                     if len(features) > 0:
                         addElement('features', features, doc, apkel)
 
-            if options.buildreport:
-                if len(app['builds']) == 0:
-                    print ("WARNING: No builds defined for " + app['id'] +
-                            " Source: " + app['Source Code'])
-                    warnings += 1
-                else:
-                    if app['Current Version Code'] != '0':
-                        gotbuild = False
-                        for build in app['builds']:
-                            if build['vercode'] == app['Current Version Code']:
-                                gotbuild = True
-                        if not gotbuild:
-                            print ("WARNING: No build data for current version of "
-                                    + app['id'] + " (" + app['Current Version']
-                                    + ") " + app['Source Code'])
-                            warnings += 1
-
-            # If we don't have the current version, check if there is a build
-            # with a commit ID starting with '!' - this means we can't build it
-            # for some reason, and don't want hassling about it...
-            if not gotcurrentver and app['Current Version Code'] != '0':
-                for build in app['builds']:
-                    if build['vercode'] == app['Current Version Code']:
-                        gotcurrentver = True
-
-            # Output a message of harassment if we don't have the current version:
-            if not gotcurrentver and app['Current Version Code'] != '0':
-                addr = app['Source Code']
-                print "WARNING: Don't have current version (" + app['Current Version'] + ") of " + app['Name']
-                print "         (" + app['id'] + ") " + addr
-                warnings += 1
-                if options.verbose:
-                    # A bit of extra debug info, basically for diagnosing
-                    # app developer mistakes:
-                    print "         Current vercode:" + app['Current Version Code']
-                    print "         Got:"
-                    for apk in apks:
-                        if apk['id'] == app['id']:
-                            print "           " + str(apk['versioncode']) + " - " + apk['version']
-                if options.interactive:
-                    print "Build data out of date for " + app['id']
-                    while True:
-                        answer = raw_input("[I]gnore, [E]dit or [Q]uit?").lower()
-                        if answer == 'i':
-                            break
-                        elif answer == 'e':
-                            subprocess.call([options.editor,
-                                os.path.join('metadata',
-                                app['id'] + '.txt')])
-                            break
-                        elif answer == 'q':
-                            sys.exit(0)
-        else:
-            apps_disabled += 1
-
-    of = open(os.path.join('repo','index.xml'), 'wb')
+    of = open(os.path.join(repodir, 'index.xml'), 'wb')
     if options.pretty:
         output = doc.toprettyxml()
     else:
@@ -698,7 +543,7 @@ def main():
         
         #Create a jar of the index...
         p = subprocess.Popen(['jar', 'cf', 'index.jar', 'index.xml'],
-            cwd='repo', stdout=subprocess.PIPE)
+            cwd=repodir, stdout=subprocess.PIPE)
         output = p.communicate()[0]
         if options.verbose:
             print output
@@ -710,7 +555,7 @@ def main():
         p = subprocess.Popen(['jarsigner', '-keystore', keystore,
             '-storepass', keystorepass, '-keypass', keypass,
             '-digestalg', 'SHA1', '-sigalg', 'MD5withRSA',
-            os.path.join('repo', 'index.jar') , repo_keyalias], stdout=subprocess.PIPE)
+            os.path.join(repodir, 'index.jar') , repo_keyalias], stdout=subprocess.PIPE)
         output = p.communicate()[0]
         if p.returncode != 0:
             print "Failed to sign index"
@@ -720,6 +565,7 @@ def main():
             print output
 
     # Copy the repo icon into the repo directory...
+    icon_dir=os.path.join(repodir ,'icons')
     iconfilename = os.path.join(icon_dir, os.path.basename(repo_icon))
     shutil.copyfile(repo_icon, iconfilename)
 
@@ -727,10 +573,185 @@ def main():
     catdata = ''
     for cat in categories:
         catdata += cat + '\n'
-    f = open('repo/categories.txt', 'w')
+    f = open(os.path.join(repodir, 'categories.txt'), 'w')
     f.write(catdata)
     f.close()
 
+
+
+def archive_old_apks(apps, apks, repodir, archivedir, keepversions):
+
+    for app in apps:
+
+        # Get a list of the apks for this app...
+        apklist = []
+        for apk in apks:
+            if apk['id'] == app['id']:
+                apklist.append(apk)
+
+        # Sort the apk list into version order...
+        apklist = sorted(apklist, key=lambda apk: apk['versioncode'], reverse=True)
+
+        if len(apklist) > keepversions:
+            for apk in apklist[keepversions:]:
+                print "Moving " + apk['apkname'] + " to archive"
+                shutil.move(os.path.join(repodir, apk['apkname']),
+                    os.path.join(archivedir, apk['apkname']))
+                if 'srcname' in apk:
+                    shutil.move(os.path.join(repodir, apk['srcname']),
+                        os.path.join(archivedir, apk['srcname']))
+                apks.remove(apk)
+
+
+def main():
+
+    # Read configuration...
+    global update_stats, archive_older
+    update_stats = False
+    archive_older = 0
+    execfile('config.py', globals())
+
+    # Parse command line...
+    global options
+    parser = OptionParser()
+    parser.add_option("-c", "--createmeta", action="store_true", default=False,
+                      help="Create skeleton metadata files that are missing")
+    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="No output, except for warnings and errors")
+    parser.add_option("-b", "--buildreport", action="store_true", default=False,
+                      help="Report on build data status")
+    parser.add_option("-i", "--interactive", default=False, action="store_true",
+                      help="Interactively ask about things that need updating.")
+    parser.add_option("-e", "--editor", default="/etc/alternatives/editor",
+                      help="Specify editor to use in interactive mode. Default "+
+                          "is /etc/alternatives/editor")
+    parser.add_option("-w", "--wiki", default=False, action="store_true",
+                      help="Update the wiki")
+    parser.add_option("", "--pretty", action="store_true", default=False,
+                      help="Produce human-readable index.xml")
+    parser.add_option("--clean", action="store_true", default=False,
+                      help="Clean update - don't uses caches, reprocess all apks")
+    (options, args) = parser.parse_args()
+
+    # Get all apps...
+    apps = common.read_metadata(verbose=options.verbose)
+
+    # Generate a list of categories...
+    categories = []
+    for app in apps:
+        cats = app['Category'].split(';')
+        for cat in cats:
+            if cat not in categories:
+                categories.append(cat)
+
+    # Read known apks data (will be updated and written back when we've finished)
+    knownapks = common.KnownApks()
+
+    # Gather information about all the apk files in the repo directory, using
+    # cached data if possible.
+    apkcachefile = os.path.join('tmp', 'apkcache')
+    if not options.clean and os.path.exists(apkcachefile):
+        with open(apkcachefile, 'rb') as cf:
+            apkcache = pickle.load(cf)
+    else:
+        apkcache = {}
+    cachechanged = False
+
+    repodirs = ['repo']
+    if archive_older != 0:
+        repodirs.append('archive')
+        if not os.path.exists('archive'):
+            os.mkdir('archive')
+
+    delete_disabled_builds(apps, apkcache, repodirs)
+
+    apks, cc = scan_apks(apps, apkcache, repodirs[0], knownapks)
+    if cc:
+        cachechanged = True
+
+    # Some information from the apks needs to be applied up to the application
+    # level. When doing this, we use the info from the most recent version's apk.
+    # We deal with figuring out when the app was added and last updated at the
+    # same time.
+    for app in apps:
+        bestver = 0
+        added = None
+        lastupdated = None
+        for apk in apks:
+            if apk['id'] == app['id']:
+                if apk['versioncode'] > bestver:
+                    bestver = apk['versioncode']
+                    bestapk = apk
+
+                if 'added' in apk:
+                    if not added or apk['added'] < added:
+                        added = apk['added']
+                    if not lastupdated or apk['added'] > lastupdated:
+                        lastupdated = apk['added']
+
+        if added:
+            app['added'] = added
+        else:
+            print "WARNING: Don't know when " + app['id'] + " was added"
+        if lastupdated:
+            app['lastupdated'] = lastupdated
+        else:
+            print "WARNING: Don't know when " + app['id'] + " was last updated"
+
+        if bestver == 0:
+            if app['Name'] is None:
+                app['Name'] = app['id']
+            app['icon'] = ''
+            if app['Disabled'] is None:
+                print "WARNING: Application " + app['id'] + " has no packages"
+        else:
+            if app['Name'] is None:
+                app['Name'] = bestapk['name']
+            app['icon'] = bestapk['icon']
+
+    # Sort the app list by name, then the web site doesn't have to by default.
+    # (we had to wait until we'd scanned the apks to do this, because mostly the
+    # name comes from there!)
+    apps = sorted(apps, key=lambda app: app['Name'].upper())
+
+    # Generate warnings for apk's with no metadata (or create skeleton
+    # metadata files, if requested on the command line)
+    for apk in apks:
+        found = False
+        for app in apps:
+            if app['id'] == apk['id']:
+                found = True
+                break
+        if not found:
+            if options.createmeta:
+                f = open(os.path.join('metadata', apk['id'] + '.txt'), 'w')
+                f.write("License:Unknown\n")
+                f.write("Web Site:\n")
+                f.write("Source Code:\n")
+                f.write("Issue Tracker:\n")
+                f.write("Summary:" + apk['name'] + "\n")
+                f.write("Description:\n")
+                f.write(apk['name'] + "\n")
+                f.write(".\n")
+                f.close()
+                print "Generated skeleton metadata for " + apk['id']
+            else:
+                print "WARNING: " + apk['apkname'] + " (" + apk['id'] + ") has no metadata"
+                print "       " + apk['name'] + " - " + apk['version']  
+
+    if len(repodirs) > 1:
+        archive_old_apks(apps, apks, repodirs[0], repodirs[1], archive_older)
+
+    make_index(apps, apks, repodirs[0], False, categories)
+
+    if len(repodirs) > 1:
+        apks, cc = scan_apks(apps, apkcache, repodirs[1], knownapks)
+        if cc:
+            cachechanged = True
+        make_index(apps, apks, repodirs[1], True, categories)
+
     if update_stats:
 
         # Update known apks info...
@@ -748,19 +769,19 @@ def main():
                         data += app['icon'] + "\t"
                         data += app['License'] + "\n"
                         break
-            f = open('repo/latestapps.dat', 'w')
+            f = open(os.path.join(repodirs[0], 'latestapps.dat'), 'w')
             f.write(data)
             f.close()
 
+    if cachechanged:
+        with open(apkcachefile, 'wb') as cf:
+            pickle.dump(apkcache, cf)
+
     # Update the wiki...
     if options.wiki:
         update_wiki(apps, apks, options.verbose)
 
     print "Finished."
-    print str(apps_inrepo) + " apps in repo"
-    print str(apps_disabled) + " disabled"
-    print str(apps_nopkg) + " with no packages"
-    print str(warnings) + " warnings"
 
 if __name__ == "__main__":
     main()