1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-04 22:40:12 +01:00

Create archive repo (configurable option)

This commit is contained in:
Ciaran Gultnieks 2013-05-09 20:09:17 +01:00
parent 5ab225e1e1
commit 48296df5b0
4 changed files with 295 additions and 235 deletions

View 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

View 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:

View 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)

View 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():
# 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()
: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]
icon_dir=os.path.join('repo','icons')
def scan_apks(apps, apkcache, repodir, knownapks):
"""Scan the apks in the given repo directory.
This also extracts the 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']
def make_index(apps, apks, repodir, archive, categories):
"""Make a repo index.
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"
: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
"""
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']
# 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()