from xml.dom.minidom import Document
from optparse import OptionParser
import time
+from pyasn1.error import PyAsn1Error
+from pyasn1.codec.der import decoder, encoder
+from pyasn1_modules import rfc2315
+from hashlib import md5
+
from PIL import Image
import logging
yield os.path.join(repodir, "icons")
-def update_wiki(apps, apks):
+def update_wiki(apps, sortedids, apks):
"""Update the wiki
:param apps: fully populated list of all applications
site.login(config['wiki_user'], config['wiki_password'])
generated_pages = {}
generated_redirects = {}
- for app in apps:
+
+ for appid in sortedids:
+ app = apps[appid]
+
wikidata = ''
if app['Disabled']:
wikidata += '{{Disabled|' + app['Disabled'] + '}}\n'
for af in app['AntiFeatures'].split(','):
wikidata += '{{AntiFeature|' + af + '}}\n'
wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|donate=%s|flattr=%s|bitcoin=%s|litecoin=%s|dogecoin=%s|license=%s|root=%s}}\n' % (
- app['id'],
+ appid,
app['Name'],
time.strftime('%Y-%m-%d', app['added']) if 'added' in app else '',
time.strftime('%Y-%m-%d', app['lastupdated']) if 'lastupdated' in app else '',
wikidata += "This app provides: %s" % ', '.join(app['Summary'].split(','))
wikidata += app['Summary']
- wikidata += " - [https://f-droid.org/repository/browse/?fdid=" + app['id'] + " view in repository]\n\n"
+ wikidata += " - [https://f-droid.org/repository/browse/?fdid=" + appid + " view in repository]\n\n"
wikidata += "=Description=\n"
wikidata += metadata.description_wiki(app['Description']) + "\n"
wikidata += "=Maintainer Notes=\n"
if 'Maintainer Notes' in app:
wikidata += metadata.description_wiki(app['Maintainer Notes']) + "\n"
- wikidata += "\nMetadata: [https://gitlab.com/fdroid/fdroiddata/blob/master/metadata/{0}.txt current] [https://gitlab.com/fdroid/fdroiddata/commits/master/metadata/{0}.txt history]\n".format(app['id'])
+ wikidata += "\nMetadata: [https://gitlab.com/fdroid/fdroiddata/blob/master/metadata/{0}.txt current] [https://gitlab.com/fdroid/fdroiddata/commits/master/metadata/{0}.txt history]\n".format(appid)
# Get a list of all packages for this application...
apklist = []
cantupdate = False
buildfails = False
for apk in apks:
- if apk['id'] == app['id']:
+ if apk['id'] == appid:
if str(apk['versioncode']) == app['Current Version Code']:
gotcurrentver = True
apklist.append(apk)
buildfails = True
apklist.append({'versioncode': int(thisbuild['vercode']),
'version': thisbuild['version'],
- 'buildproblem': "The build for this version appears to have failed. Check the [[{0}/lastbuild|build log]].".format(app['id'])
+ 'buildproblem': "The build for this version appears to have failed. Check the [[{0}/lastbuild_{1}|build log]].".format(appid, thisbuild['vercode'])
})
if app['Current Version Code'] == '0':
cantupdate = True
# We can't have underscores in the page name, even if they're in
# the package ID, because MediaWiki messes with them...
- pagename = app['id'].replace('_', ' ')
+ pagename = appid.replace('_', ' ')
# Drop a trailing newline, because mediawiki is going to drop it anyway
# and it we don't we'll think the page has changed when it hasn't...
:param apkcache: current apk cache information
:param repodirs: the repo directories to process
"""
- for app in apps:
+ for appid, app in apps.iteritems():
for build in app['builds']:
if build['disable']:
- apkfilename = app['id'] + '_' + str(build['vercode']) + '.apk'
+ apkfilename = appid + '_' + str(build['vercode']) + '.apk'
for repodir in repodirs:
apkpath = os.path.join(repodir, apkfilename)
+ ascpath = apkpath + ".asc"
srcpath = os.path.join(repodir, apkfilename[:-4] + "_src.tar.gz")
- for name in [apkpath, srcpath]:
+ for name in [apkpath, srcpath, ascpath]:
if os.path.exists(name):
logging.warn("Deleting disabled build output " + apkfilename)
os.remove(name)
resize_icon(iconpath, density)
+cert_path_regex = re.compile(r'^META-INF/.*\.RSA$')
+
+
+def getsig(apkpath):
+ """ Get the signing certificate of an apk. To get the same md5 has that
+ Android gets, we encode the .RSA certificate in a specific format and pass
+ it hex-encoded to the md5 digest algorithm.
+
+ :param apkpath: path to the apk
+ :returns: A string containing the md5 of the signature of the apk or None
+ if an error occurred.
+ """
+
+ cert = None
+
+ with zipfile.ZipFile(apkpath, 'r') as apk:
+
+ certs = [n for n in apk.namelist() if cert_path_regex.match(n)]
+
+ if len(certs) < 1:
+ logging.error("Found no signing certificates on %s" % apkpath)
+ return None
+ if len(certs) > 1:
+ logging.error("Found multiple signing certificates on %s" % apkpath)
+ return None
+
+ cert = apk.read(certs[0])
+
+ content = decoder.decode(cert, asn1Spec=rfc2315.ContentInfo())[0]
+ if content.getComponentByName('contentType') != rfc2315.signedData:
+ logging.error("Unexpected format.")
+ return None
+
+ content = decoder.decode(content.getComponentByName('content'),
+ asn1Spec=rfc2315.SignedData())[0]
+ try:
+ certificates = content.getComponentByName('certificates')
+ except PyAsn1Error:
+ logging.error("Certificates not found.")
+ return None
+
+ cert_encoded = encoder.encode(certificates)[4:]
+
+ return md5(cert_encoded.encode('hex')).hexdigest()
+
+
def scan_apks(apps, apkcache, repodir, knownapks):
"""Scan the apks in the given repo directory.
apkfilename = apkfile[len(repodir) + 1:]
if ' ' in apkfilename:
- logging.error("No spaces in APK filenames!")
+ logging.critical("Spaces in filenames are not allowed.")
sys.exit(1)
if apkfilename in apkcache:
if os.path.exists(os.path.join(repodir, srcfilename)):
thisinfo['srcname'] = srcfilename
thisinfo['size'] = os.path.getsize(apkfile)
- thisinfo['permissions'] = []
- thisinfo['features'] = []
+ thisinfo['permissions'] = set()
+ thisinfo['features'] = set()
thisinfo['icons_src'] = {}
thisinfo['icons'] = {}
- p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
- config['build_tools'], 'aapt'),
- 'dump', 'badging', apkfile])
+ p = SilentPopen([config['aapt'], 'dump', 'badging', apkfile])
if p.returncode != 0:
if options.delete_unknown:
if os.path.exists(apkfile):
thisinfo['versioncode'] = int(re.match(vercode_pat, line).group(1))
thisinfo['version'] = re.match(vername_pat, line).group(1)
except Exception, e:
- logging.info("Package matching failed: " + str(e))
+ logging.error("Package matching failed: " + str(e))
logging.info("Line was: " + line)
sys.exit(1)
elif line.startswith("application:"):
perm = re.match(string_pat, line).group(1)
if perm.startswith("android.permission."):
perm = perm[19:]
- thisinfo['permissions'].append(perm)
+ thisinfo['permissions'].add(perm)
elif line.startswith("uses-feature:"):
perm = re.match(string_pat, line).group(1)
# Filter out this, it's only added with the latest SDK tools and
and perm != "android.hardware.screen.landscape":
if perm.startswith("android.feature."):
perm = perm[16:]
- thisinfo['features'].append(perm)
+ thisinfo['features'].add(perm)
if 'sdkversion' not in thisinfo:
- logging.warn("no SDK version information found")
+ logging.warn("No SDK version information found in {0}".format(apkfile))
thisinfo['sdkversion'] = 0
# Check for debuggable apks...
if common.isApkDebuggable(apkfile, config):
- logging.warn('{0} is set to android:debuggable="true"!'.format(apkfile))
+ logging.warn('{0} is set to android:debuggable="true"'.format(apkfile))
# Calculate the sha256...
sha = hashlib.sha256()
sha.update(t)
thisinfo['sha256'] = sha.hexdigest()
- # Get the signature (or md5 of, to be precise)...
- getsig_dir = os.path.join(os.path.dirname(__file__), 'getsig')
- if not os.path.exists(getsig_dir + "/getsig.class"):
- logging.critical("getsig.class not found. To fix: cd '%s' && ./make.sh" % getsig_dir)
+ # verify the jar signature is correct
+ args = ['jarsigner', '-verify']
+ if options.verbose:
+ args += ['-verbose', '-certs']
+ args += apkfile
+ p = FDroidPopen(args)
+ if p.returncode != 0:
+ logging.critical(apkfile + " has a bad signature!")
sys.exit(1)
- p = FDroidPopen(['java', '-cp', os.path.join(os.path.dirname(__file__), 'getsig'),
- 'getsig', os.path.join(os.getcwd(), apkfile)])
- if p.returncode != 0 or not p.output.startswith('Result:'):
+
+ # Get the signature (or md5 of, to be precise)...
+ thisinfo['sig'] = getsig(os.path.join(os.getcwd(), apkfile))
+ if not thisinfo['sig']:
logging.critical("Failed to get apk signature")
sys.exit(1)
- thisinfo['sig'] = p.output[7:].strip()
apk = zipfile.ZipFile(apkfile, 'r')
continue
if last_density is None:
continue
- logging.info("Density %s not available, resizing down from %s"
- % (density, last_density))
+ logging.debug("Density %s not available, resizing down from %s"
+ % (density, last_density))
last_iconpath = os.path.join(
get_icon_dir(repodir, last_density), iconfilename)
continue
if last_density is None:
continue
- logging.info("Density %s not available, copying from lower density %s"
- % (density, last_density))
+ logging.debug("Density %s not available, copying from lower density %s"
+ % (density, last_density))
shutil.copyfile(
os.path.join(get_icon_dir(repodir, last_density), iconfilename),
repo_pubkey_fingerprint = None
-def make_index(apps, apks, repodir, archive, categories):
+def make_index(apps, sortedids, apks, repodir, archive, categories):
"""Make a repo index.
:param apps: fully populated apps list
root.appendChild(repoel)
- for app in apps:
+ for appid in sortedids:
+ app = apps[appid]
if app['Disabled'] is not None:
continue
# Get a list of the apks for this app...
apklist = []
for apk in apks:
- if apk['id'] == app['id']:
+ if apk['id'] == appid:
apklist.append(apk)
if len(apklist) == 0:
if app['icon']:
addElement('icon', app['icon'], doc, apel)
- def linkres(link):
- for app in apps:
- if app['id'] == link:
- return ("fdroid.app:" + link, app['Name'])
- raise MetaDataException("Cannot resolve app id " + link)
+ def linkres(appid):
+ if appid in apps:
+ return ("fdroid.app:" + appid, apps[appid]['Name'])
+ raise MetaDataException("Cannot resolve app id " + appid)
+
addElement('desc',
metadata.description_html(app['Description'], linkres),
doc, apel)
addElement('added', time.strftime('%Y-%m-%d', apk['added']), doc, apkel)
if app['Requires Root']:
if 'ACCESS_SUPERUSER' not in apk['permissions']:
- apk['permissions'].append('ACCESS_SUPERUSER')
+ apk['permissions'].add('ACCESS_SUPERUSER')
if len(apk['permissions']) > 0:
addElement('permissions', ','.join(apk['permissions']), doc, apkel)
p = FDroidPopen(args)
# TODO keypass should be sent via stdin
if p.returncode != 0:
- logging.info("Failed to sign index")
+ logging.critical("Failed to sign index")
sys.exit(1)
# Copy the repo icon into the repo directory...
def archive_old_apks(apps, apks, archapks, repodir, archivedir, defaultkeepversions):
- for app in apps:
+ for appid, app in apps.iteritems():
# Get a list of the apks for this app...
apklist = []
for apk in apks:
- if apk['id'] == app['id']:
+ if apk['id'] == appid:
apklist.append(apk)
# Sort the apk list into version order...
if 'srcname' in apk:
shutil.move(os.path.join(repodir, apk['srcname']),
os.path.join(archivedir, apk['srcname']))
- # Move GPG signature too...
- sigfile = apk['srcname'] + '.asc'
- sigsrc = os.path.join(repodir, sigfile)
- if os.path.exists(sigsrc):
- shutil.move(sigsrc, os.path.join(archivedir, sigfile))
+ # Move GPG signature too...
+ sigfile = apk['srcname'] + '.asc'
+ sigsrc = os.path.join(repodir, sigfile)
+ if os.path.exists(sigsrc):
+ shutil.move(sigsrc, os.path.join(archivedir, sigfile))
archapks.append(apk)
apks.remove(apk)
resize_all_icons(repodirs)
sys.exit(0)
+ # check that icons exist now, rather than fail at the end of `fdroid update`
+ for k in ['repo_icon', 'archive_icon']:
+ if k in config:
+ if not os.path.exists(config[k]):
+ logging.critical(k + ' "' + config[k] + '" does not exist! Correct it in config.py.')
+ sys.exit(1)
+
# Get all apps...
apps = metadata.read_metadata()
# Generate a list of categories...
categories = set()
- for app in apps:
+ for app in apps.itervalues():
categories.update(app['Categories'])
# Read known apks data (will be updated and written back when we've finished)
# metadata files, if requested on the command line)
newmetadata = False
for apk in apks:
- found = False
- for app in apps:
- if app['id'] == apk['id']:
- found = True
- break
- if not found:
+ if apk['id'] not in apps:
if options.create_metadata:
if 'name' not in apk:
logging.error(apk['id'] + ' does not have a name! Skipping...')
# 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:
+ for appid, app in apps.iteritems():
bestver = 0
added = None
lastupdated = None
for apk in apks + archapks:
- if apk['id'] == app['id']:
+ if apk['id'] == appid:
if apk['versioncode'] > bestver:
bestver = apk['versioncode']
bestapk = apk
if added:
app['added'] = added
else:
- logging.warn("Don't know when " + app['id'] + " was added")
+ logging.warn("Don't know when " + appid + " was added")
if lastupdated:
app['lastupdated'] = lastupdated
else:
- logging.warn("Don't know when " + app['id'] + " was last updated")
+ logging.warn("Don't know when " + appid + " was last updated")
if bestver == 0:
if app['Name'] is None:
- app['Name'] = app['id']
+ app['Name'] = app['Auto Name'] or appid
app['icon'] = None
- logging.warn("Application " + app['id'] + " has no packages")
+ logging.warn("Application " + appid + " has no packages")
else:
if app['Name'] is None:
app['Name'] = bestapk['name']
# 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())
+ sortedids = sorted(apps.iterkeys(), key=lambda appid: apps[appid]['Name'].upper())
if len(repodirs) > 1:
archive_old_apks(apps, apks, archapks, repodirs[0], repodirs[1], config['archive_older'])
# Make the index for the main repo...
- make_index(apps, apks, repodirs[0], False, categories)
+ make_index(apps, sortedids, apks, repodirs[0], False, categories)
# If there's an archive repo, make the index for it. We already scanned it
# earlier on.
if len(repodirs) > 1:
- make_index(apps, archapks, repodirs[1], True, categories)
+ make_index(apps, sortedids, archapks, repodirs[1], True, categories)
if config['update_stats']:
for line in file(os.path.join('stats', 'latestapps.txt')):
appid = line.rstrip()
data += appid + "\t"
- for app in apps:
- if app['id'] == appid:
- data += app['Name'] + "\t"
- if app['icon'] is not None:
- data += app['icon'] + "\t"
- data += app['License'] + "\n"
- break
+ app = apps[appid]
+ data += app['Name'] + "\t"
+ if app['icon'] is not None:
+ data += app['icon'] + "\t"
+ data += app['License'] + "\n"
f = open(os.path.join(repodirs[0], 'latestapps.dat'), 'w')
f.write(data)
f.close()
# Update the wiki...
if options.wiki:
- update_wiki(apps, apks + archapks)
+ update_wiki(apps, sortedids, apks + archapks)
logging.info("Finished.")