3 # update.py - part of the FDroid server tools
4 # Copyright (C) 2016, Blue Jay Wireless
5 # Copyright (C) 2014-2016, Hans-Christoph Steiner <hans@eds.org>
6 # Copyright (C) 2010-2015, Ciaran Gultnieks <ciaran@ciarang.com>
7 # Copyright (C) 2013-2014, Daniel Martà <mvdan@mvdan.cc>
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Affero General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Affero General Public License for more details.
19 # You should have received a copy of the GNU Affero General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
31 from datetime import datetime, timedelta
32 from argparse import ArgumentParser
35 from binascii import hexlify
43 from . import metadata
44 from .common import SdkToolsPopen
48 # less than the valid range of versionCode, i.e. Java's Integer.MIN_VALUE
49 UNSET_VERSION_CODE = -0x100000000
51 APK_NAME_PAT = re.compile(".*name='([a-zA-Z0-9._]*)'.*")
52 APK_VERCODE_PAT = re.compile(".*versionCode='([0-9]*)'.*")
53 APK_VERNAME_PAT = re.compile(".*versionName='([^']*)'.*")
54 APK_LABEL_PAT = re.compile(".*label='(.*?)'(\n| [a-z]*?=).*")
55 APK_ICON_PAT = re.compile(".*application-icon-([0-9]+):'([^']+?)'.*")
56 APK_ICON_PAT_NODPI = re.compile(".*icon='([^']+?)'.*")
57 APK_SDK_VERSION_PAT = re.compile(".*'([0-9]*)'.*")
58 APK_PERMISSION_PAT = \
59 re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*")
60 APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*")
62 screen_densities = ['640', '480', '320', '240', '160', '120']
64 all_screen_densities = ['0'] + screen_densities
66 UsesPermission = collections.namedtuple('UsesPermission', ['name', 'maxSdkVersion'])
67 UsesPermissionSdk23 = collections.namedtuple('UsesPermissionSdk23', ['name', 'maxSdkVersion'])
69 ALLOWED_EXTENSIONS = ('png', 'jpg', 'jpeg')
70 GRAPHIC_NAMES = ('featureGraphic', 'icon', 'promoGraphic', 'tvBanner')
71 SCREENSHOT_DIRS = ('phoneScreenshots', 'sevenInchScreenshots',
72 'tenInchScreenshots', 'tvScreenshots', 'wearScreenshots')
75 def dpi_to_px(density):
76 return (int(density) * 48) / 160
80 return (int(px) * 160) / 48
83 def get_icon_dir(repodir, density):
85 return os.path.join(repodir, "icons")
86 return os.path.join(repodir, "icons-%s" % density)
89 def get_icon_dirs(repodir):
90 for density in screen_densities:
91 yield get_icon_dir(repodir, density)
94 def get_all_icon_dirs(repodir):
95 for density in all_screen_densities:
96 yield get_icon_dir(repodir, density)
99 def update_wiki(apps, sortedids, apks):
102 :param apps: fully populated list of all applications
103 :param apks: all apks, except...
105 logging.info("Updating wiki")
107 wikiredircat = 'App Redirects'
109 site = mwclient.Site((config['wiki_protocol'], config['wiki_server']),
110 path=config['wiki_path'])
111 site.login(config['wiki_user'], config['wiki_password'])
113 generated_redirects = {}
115 for appid in sortedids:
116 app = metadata.App(apps[appid])
120 wikidata += '{{Disabled|' + app.Disabled + '}}\n'
122 for af in app.AntiFeatures:
123 wikidata += '{{AntiFeature|' + af + '}}\n'
128 wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % (
131 app.added.strftime('%Y-%m-%d') if app.added else '',
132 app.lastUpdated.strftime('%Y-%m-%d') if app.lastUpdated else '',
147 wikidata += "This app provides: %s" % ', '.join(app.Summary.split(','))
149 wikidata += app.Summary
150 wikidata += " - [https://f-droid.org/repository/browse/?fdid=" + appid + " view in repository]\n\n"
152 wikidata += "=Description=\n"
153 wikidata += metadata.description_wiki(app.Description) + "\n"
155 wikidata += "=Maintainer Notes=\n"
156 if app.MaintainerNotes:
157 wikidata += metadata.description_wiki(app.MaintainerNotes) + "\n"
158 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)
160 # Get a list of all packages for this application...
162 gotcurrentver = False
166 if apk['packageName'] == appid:
167 if str(apk['versionCode']) == app.CurrentVersionCode:
170 # Include ones we can't build, as a special case...
171 for build in app.builds:
173 if build.versionCode == app.CurrentVersionCode:
175 # TODO: Nasty: vercode is a string in the build, and an int elsewhere
176 apklist.append({'versionCode': int(build.versionCode),
177 'versionName': build.versionName,
178 'buildproblem': "The build for this version was manually disabled. Reason: {0}".format(build.disable),
183 if apk['versionCode'] == int(build.versionCode):
188 apklist.append({'versionCode': int(build.versionCode),
189 'versionName': build.versionName,
190 'buildproblem': "The build for this version appears to have failed. Check the [[{0}/lastbuild_{1}|build log]].".format(appid, build.versionCode),
192 if app.CurrentVersionCode == '0':
194 # Sort with most recent first...
195 apklist = sorted(apklist, key=lambda apk: apk['versionCode'], reverse=True)
197 wikidata += "=Versions=\n"
198 if len(apklist) == 0:
199 wikidata += "We currently have no versions of this app available."
200 elif not gotcurrentver:
201 wikidata += "We don't have the current version of this app."
203 wikidata += "We have the current version of this app."
204 wikidata += " (Check mode: " + app.UpdateCheckMode + ") "
205 wikidata += " (Auto-update mode: " + app.AutoUpdateMode + ")\n\n"
206 if len(app.NoSourceSince) > 0:
207 wikidata += "This application has partially or entirely been missing source code since version " + app.NoSourceSince + ".\n\n"
208 if len(app.CurrentVersion) > 0:
209 wikidata += "The current (recommended) version is " + app.CurrentVersion
210 wikidata += " (version code " + app.CurrentVersionCode + ").\n\n"
213 wikidata += "==" + apk['versionName'] + "==\n"
215 if 'buildproblem' in apk:
216 wikidata += "We can't build this version: " + apk['buildproblem'] + "\n\n"
219 wikidata += "This version is built and signed by "
221 wikidata += "F-Droid, and guaranteed to correspond to the source tarball published with it.\n\n"
223 wikidata += "the original developer.\n\n"
224 wikidata += "Version code: " + str(apk['versionCode']) + '\n'
226 wikidata += '\n[[Category:' + wikicat + ']]\n'
227 if len(app.NoSourceSince) > 0:
228 wikidata += '\n[[Category:Apps missing source code]]\n'
229 if validapks == 0 and not app.Disabled:
230 wikidata += '\n[[Category:Apps with no packages]]\n'
231 if cantupdate and not app.Disabled:
232 wikidata += "\n[[Category:Apps we cannot update]]\n"
233 if buildfails and not app.Disabled:
234 wikidata += "\n[[Category:Apps with failing builds]]\n"
235 elif not gotcurrentver and not cantupdate and not app.Disabled and app.UpdateCheckMode != "Static":
236 wikidata += '\n[[Category:Apps to Update]]\n'
238 wikidata += '\n[[Category:Apps that are disabled]]\n'
239 if app.UpdateCheckMode == 'None' and not app.Disabled:
240 wikidata += '\n[[Category:Apps with no update check]]\n'
241 for appcat in app.Categories:
242 wikidata += '\n[[Category:{0}]]\n'.format(appcat)
244 # We can't have underscores in the page name, even if they're in
245 # the package ID, because MediaWiki messes with them...
246 pagename = appid.replace('_', ' ')
248 # Drop a trailing newline, because mediawiki is going to drop it anyway
249 # and it we don't we'll think the page has changed when it hasn't...
250 if wikidata.endswith('\n'):
251 wikidata = wikidata[:-1]
253 generated_pages[pagename] = wikidata
255 # Make a redirect from the name to the ID too, unless there's
256 # already an existing page with the name and it isn't a redirect.
258 apppagename = app.Name.replace('_', ' ')
259 apppagename = apppagename.replace('{', '')
260 apppagename = apppagename.replace('}', ' ')
261 apppagename = apppagename.replace(':', ' ')
262 apppagename = apppagename.replace('[', ' ')
263 apppagename = apppagename.replace(']', ' ')
264 # Drop double spaces caused mostly by replacing ':' above
265 apppagename = apppagename.replace(' ', ' ')
266 for expagename in site.allpages(prefix=apppagename,
267 filterredir='nonredirects',
269 if expagename == apppagename:
271 # Another reason not to make the redirect page is if the app name
272 # is the same as it's ID, because that will overwrite the real page
273 # with an redirect to itself! (Although it seems like an odd
274 # scenario this happens a lot, e.g. where there is metadata but no
275 # builds or binaries to extract a name from.
276 if apppagename == pagename:
279 generated_redirects[apppagename] = "#REDIRECT [[" + pagename + "]]\n[[Category:" + wikiredircat + "]]"
281 for tcat, genp in [(wikicat, generated_pages),
282 (wikiredircat, generated_redirects)]:
283 catpages = site.Pages['Category:' + tcat]
285 for page in catpages:
286 existingpages.append(page.name)
287 if page.name in genp:
288 pagetxt = page.edit()
289 if pagetxt != genp[page.name]:
290 logging.debug("Updating modified page " + page.name)
291 page.save(genp[page.name], summary='Auto-updated')
293 logging.debug("Page " + page.name + " is unchanged")
295 logging.warn("Deleting page " + page.name)
296 page.delete('No longer published')
297 for pagename, text in genp.items():
298 logging.debug("Checking " + pagename)
299 if pagename not in existingpages:
300 logging.debug("Creating page " + pagename)
302 newpage = site.Pages[pagename]
303 newpage.save(text, summary='Auto-created')
304 except Exception as e:
305 logging.error("...FAILED to create page '{0}': {1}".format(pagename, e))
307 # Purge server cache to ensure counts are up to date
308 site.pages['Repository Maintenance'].purge()
311 def delete_disabled_builds(apps, apkcache, repodirs):
312 """Delete disabled build outputs.
314 :param apps: list of all applications, as per metadata.read_metadata
315 :param apkcache: current apk cache information
316 :param repodirs: the repo directories to process
318 for appid, app in apps.items():
319 for build in app['builds']:
320 if not build.disable:
322 apkfilename = common.get_release_filename(app, build)
323 iconfilename = "%s.%s.png" % (
326 for repodir in repodirs:
328 os.path.join(repodir, apkfilename),
329 os.path.join(repodir, apkfilename + '.asc'),
330 os.path.join(repodir, apkfilename[:-4] + "_src.tar.gz"),
332 for density in all_screen_densities:
333 repo_dir = get_icon_dir(repodir, density)
334 files.append(os.path.join(repo_dir, iconfilename))
337 if os.path.exists(f):
338 logging.info("Deleting disabled build output " + f)
340 if apkfilename in apkcache:
341 del apkcache[apkfilename]
344 def resize_icon(iconpath, density):
346 if not os.path.isfile(iconpath):
351 fp = open(iconpath, 'rb')
353 size = dpi_to_px(density)
355 if any(length > size for length in im.size):
357 im.thumbnail((size, size), Image.ANTIALIAS)
358 logging.debug("%s was too large at %s - new size is %s" % (
359 iconpath, oldsize, im.size))
360 im.save(iconpath, "PNG")
362 except Exception as e:
363 logging.error("Failed resizing {0} - {1}".format(iconpath, e))
370 def resize_all_icons(repodirs):
371 """Resize all icons that exceed the max size
373 :param repodirs: the repo directories to process
375 for repodir in repodirs:
376 for density in screen_densities:
377 icon_dir = get_icon_dir(repodir, density)
378 icon_glob = os.path.join(icon_dir, '*.png')
379 for iconpath in glob.glob(icon_glob):
380 resize_icon(iconpath, density)
384 """ Get the signing certificate of an apk. To get the same md5 has that
385 Android gets, we encode the .RSA certificate in a specific format and pass
386 it hex-encoded to the md5 digest algorithm.
388 :param apkpath: path to the apk
389 :returns: A string containing the md5 of the signature of the apk or None
390 if an error occurred.
393 # verify the jar signature is correct
394 if not common.verify_apk_signature(apkpath):
397 with zipfile.ZipFile(apkpath, 'r') as apk:
398 certs = [n for n in apk.namelist() if common.CERT_PATH_REGEX.match(n)]
401 logging.error("Found no signing certificates on %s" % apkpath)
404 logging.error("Found multiple signing certificates on %s" % apkpath)
407 cert = apk.read(certs[0])
409 cert_encoded = common.get_certificate(cert)
411 return hashlib.md5(hexlify(cert_encoded)).hexdigest()
414 def get_cache_file():
415 return os.path.join('tmp', 'apkcache')
420 Gather information about all the apk files in the repo directory,
421 using cached data if possible.
424 apkcachefile = get_cache_file()
425 if not options.clean and os.path.exists(apkcachefile):
426 with open(apkcachefile, 'rb') as cf:
427 apkcache = pickle.load(cf, encoding='utf-8')
428 if apkcache.get("METADATA_VERSION") != METADATA_VERSION:
436 def write_cache(apkcache):
437 apkcachefile = get_cache_file()
438 cache_path = os.path.dirname(apkcachefile)
439 if not os.path.exists(cache_path):
440 os.makedirs(cache_path)
441 apkcache["METADATA_VERSION"] = METADATA_VERSION
442 with open(apkcachefile, 'wb') as cf:
443 pickle.dump(apkcache, cf)
446 def get_icon_bytes(apkzip, iconsrc):
447 '''ZIP has no official encoding, UTF-* and CP437 are defacto'''
449 return apkzip.read(iconsrc)
451 return apkzip.read(iconsrc.encode('utf-8').decode('cp437'))
454 def sha256sum(filename):
455 '''Calculate the sha256 of the given file'''
456 sha = hashlib.sha256()
457 with open(filename, 'rb') as f:
463 return sha.hexdigest()
466 def has_old_openssl(filename):
467 '''checks for known vulnerable openssl versions in the APK'''
469 # statically load this pattern
470 if not hasattr(has_old_openssl, "pattern"):
471 has_old_openssl.pattern = re.compile(b'.*OpenSSL ([01][0-9a-z.-]+)')
473 with zipfile.ZipFile(filename) as zf:
474 for name in zf.namelist():
475 if name.endswith('libcrypto.so') or name.endswith('libssl.so'):
478 chunk = lib.read(4096)
481 m = has_old_openssl.pattern.search(chunk)
483 version = m.group(1).decode('ascii')
484 if version.startswith('1.0.1') and version[5] >= 'r' \
485 or version.startswith('1.0.2') and version[5] >= 'f':
486 logging.debug('"%s" contains recent %s (%s)', filename, name, version)
488 logging.warning('"%s" contains outdated %s (%s)', filename, name, version)
494 def insert_obbs(repodir, apps, apks):
495 """Scans the .obb files in a given repo directory and adds them to the
496 relevant APK instances. OBB files have versionCodes like APK
497 files, and they are loosely associated. If there is an OBB file
498 present, then any APK with the same or higher versionCode will use
499 that OBB file. There are two OBB types: main and patch, each APK
500 can only have only have one of each.
502 https://developer.android.com/google/play/expansion-files.html
504 :param repodir: repo directory to scan
505 :param apps: list of current, valid apps
506 :param apks: current information on all APKs
510 def obbWarnDelete(f, msg):
511 logging.warning(msg + f)
512 if options.delete_unknown:
513 logging.error("Deleting unknown file: " + f)
517 java_Integer_MIN_VALUE = -pow(2, 31)
518 currentPackageNames = apps.keys()
519 for f in glob.glob(os.path.join(repodir, '*.obb')):
520 obbfile = os.path.basename(f)
521 # obbfile looks like: [main|patch].<expansion-version>.<package-name>.obb
522 chunks = obbfile.split('.')
523 if chunks[0] != 'main' and chunks[0] != 'patch':
524 obbWarnDelete(f, 'OBB filename must start with "main." or "patch.": ')
526 if not re.match(r'^-?[0-9]+$', chunks[1]):
527 obbWarnDelete('The OBB version code must come after "' + chunks[0] + '.": ')
529 versionCode = int(chunks[1])
530 packagename = ".".join(chunks[2:-1])
532 highestVersionCode = java_Integer_MIN_VALUE
533 if packagename not in currentPackageNames:
534 obbWarnDelete(f, "OBB's packagename does not match a supported APK: ")
537 if packagename == apk['packageName'] and apk['versionCode'] > highestVersionCode:
538 highestVersionCode = apk['versionCode']
539 if versionCode > highestVersionCode:
540 obbWarnDelete(f, 'OBB file has newer versionCode(' + str(versionCode)
541 + ') than any APK: ')
543 obbsha256 = sha256sum(f)
544 obbs.append((packagename, versionCode, obbfile, obbsha256))
547 for (packagename, versionCode, obbfile, obbsha256) in sorted(obbs, reverse=True):
548 if versionCode <= apk['versionCode'] and packagename == apk['packageName']:
549 if obbfile.startswith('main.') and 'obbMainFile' not in apk:
550 apk['obbMainFile'] = obbfile
551 apk['obbMainFileSha256'] = obbsha256
552 elif obbfile.startswith('patch.') and 'obbPatchFile' not in apk:
553 apk['obbPatchFile'] = obbfile
554 apk['obbPatchFileSha256'] = obbsha256
555 if 'obbMainFile' in apk and 'obbPatchFile' in apk:
559 def _get_localized_dict(app, locale):
560 '''get the dict to add localized store metadata to'''
561 if 'localized' not in app:
562 app['localized'] = collections.OrderedDict()
563 if locale not in app['localized']:
564 app['localized'][locale] = collections.OrderedDict()
565 return app['localized'][locale]
568 def _set_localized_text_entry(app, locale, key, f):
569 limit = config['char_limits'][key]
570 localized = _get_localized_dict(app, locale)
572 text = fp.read()[:limit]
574 localized[key] = text
577 def _set_author_entry(app, key, f):
578 limit = config['char_limits']['author']
580 text = fp.read()[:limit]
585 def copy_triple_t_store_metadata(apps):
586 """Include store metadata from the app's source repo
588 The Triple-T Gradle Play Publisher is a plugin that has a standard
589 file layout for all of the metadata and graphics that the Google
590 Play Store accepts. Since F-Droid has the git repo, it can just
591 pluck those files directly. This method reads any text files into
592 the app dict, then copies any graphics into the fdroid repo
595 This needs to be run before insert_localized_app_metadata() so that
596 the graphics files that are copied into the fdroid repo get
599 https://github.com/Triple-T/gradle-play-publisher#upload-images
600 https://github.com/Triple-T/gradle-play-publisher#play-store-metadata
604 if not os.path.isdir('build'):
605 return # nothing to do
607 for packageName, app in apps.items():
608 for d in glob.glob(os.path.join('build', packageName, '*', 'src', '*', 'play')):
609 logging.debug('Triple-T Gradle Play Publisher: ' + d)
610 for root, dirs, files in os.walk(d):
611 segments = root.split('/')
612 locale = segments[-2]
614 if f == 'fulldescription':
615 _set_localized_text_entry(app, locale, 'description',
616 os.path.join(root, f))
618 elif f == 'shortdescription':
619 _set_localized_text_entry(app, locale, 'summary',
620 os.path.join(root, f))
623 _set_localized_text_entry(app, locale, 'name',
624 os.path.join(root, f))
627 _set_localized_text_entry(app, locale, 'video',
628 os.path.join(root, f))
630 elif f == 'whatsnew':
631 _set_localized_text_entry(app, segments[-1], 'whatsNew',
632 os.path.join(root, f))
634 elif f == 'contactEmail':
635 _set_author_entry(app, 'authorEmail', os.path.join(root, f))
637 elif f == 'contactPhone':
638 _set_author_entry(app, 'authorPhone', os.path.join(root, f))
640 elif f == 'contactWebsite':
641 _set_author_entry(app, 'authorWebSite', os.path.join(root, f))
644 base, extension = common.get_extension(f)
645 dirname = os.path.basename(root)
646 if dirname in GRAPHIC_NAMES and extension in ALLOWED_EXTENSIONS:
647 if segments[-2] == 'listing':
648 locale = segments[-3]
650 locale = segments[-2]
651 destdir = os.path.join('repo', packageName, locale)
652 os.makedirs(destdir, mode=0o755, exist_ok=True)
653 sourcefile = os.path.join(root, f)
654 destfile = os.path.join(destdir, dirname + '.' + extension)
655 logging.debug('copying ' + sourcefile + ' ' + destfile)
656 shutil.copy(sourcefile, destfile)
659 def insert_localized_app_metadata(apps):
660 """scans standard locations for graphics and localized text
662 Scans for localized description files, store graphics, and
663 screenshot PNG files in statically defined screenshots directory
664 and adds them to the app metadata. The screenshots and graphic
665 must be PNG or JPEG files ending with ".png", ".jpg", or ".jpeg"
666 and must be in the following layout:
667 # TODO replace these docs with link to All_About_Descriptions_Graphics_and_Screenshots
668 # TODO mention that the 'localized' section is not in metadata.yml, so key names are like Java vars: camelCase with first letter lowercase.
669 repo/packageName/locale/featureGraphic.png
670 repo/packageName/locale/phoneScreenshots/1.png
671 repo/packageName/locale/phoneScreenshots/2.png
673 The changelog files must be text files named with the versionCode
674 ending with ".txt" and must be in the following layout:
675 https://github.com/fastlane/fastlane/blob/1.109.0/supply/README.md#changelogs-whats-new
677 repo/packageName/locale/changelogs/12345.txt
679 This will scan the each app's source repo then the metadata/ dir
680 for these standard locations of changelog files. If it finds
681 them, they will be added to the dict of all packages, with the
682 versions in the metadata/ folder taking precendence over the what
683 is in the app's source repo.
685 Where "packageName" is the app's packageName and "locale" is the locale
686 of the graphics, e.g. what language they are in, using the IETF RFC5646
687 format (en-US, fr-CA, es-MX, etc).
689 This will also scan the app's git for a fastlane folder, and the
690 metadata/ folder and the apps' source repos for standard locations
691 of graphic and screenshot files. If it finds them, it will copy
692 them into the repo. The fastlane files follow this pattern:
693 https://github.com/fastlane/fastlane/blob/1.109.0/supply/README.md#images-and-screenshots
697 sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
698 sourcedirs += glob.glob(os.path.join('metadata', '[A-Za-z]*', '[a-z][a-z]*'))
700 for d in sorted(sourcedirs):
701 if not os.path.isdir(d):
703 for root, dirs, files in os.walk(d):
704 segments = root.split('/')
705 packageName = segments[1]
706 if packageName not in apps:
707 logging.debug(packageName + ' does not have app metadata, skipping l18n scan.')
709 locale = segments[-1]
710 destdir = os.path.join('repo', packageName, locale)
712 if f == 'full_description.txt':
713 _set_localized_text_entry(apps[packageName], locale, 'description',
714 os.path.join(root, f))
716 elif f == 'short_description.txt':
717 _set_localized_text_entry(apps[packageName], locale, 'summary',
718 os.path.join(root, f))
720 elif f == 'title.txt':
721 _set_localized_text_entry(apps[packageName], locale, 'name',
722 os.path.join(root, f))
724 elif f == 'video.txt':
725 _set_localized_text_entry(apps[packageName], locale, 'video',
726 os.path.join(root, f))
728 elif f == str(apps[packageName]['CurrentVersionCode']) + '.txt':
729 _set_localized_text_entry(apps[packageName], segments[-2], 'whatsNew',
730 os.path.join(root, f))
733 base, extension = common.get_extension(f)
734 if base in GRAPHIC_NAMES and extension in ALLOWED_EXTENSIONS:
735 os.makedirs(destdir, mode=0o755, exist_ok=True)
736 logging.debug('copying ' + os.path.join(root, f) + ' ' + destdir)
737 shutil.copy(os.path.join(root, f), destdir)
739 if d in SCREENSHOT_DIRS:
740 for f in glob.glob(os.path.join(root, d, '*.*')):
741 _, extension = common.get_extension(f)
742 if extension in ALLOWED_EXTENSIONS:
743 screenshotdestdir = os.path.join(destdir, d)
744 os.makedirs(screenshotdestdir, mode=0o755, exist_ok=True)
745 logging.debug('copying ' + f + ' ' + screenshotdestdir)
746 shutil.copy(f, screenshotdestdir)
748 repofiles = sorted(glob.glob(os.path.join('repo', '[A-Za-z]*', '[a-z][a-z][A-Z-.@]*')))
750 if not os.path.isdir(d):
752 for f in sorted(glob.glob(os.path.join(d, '*.*')) + glob.glob(os.path.join(d, '*Screenshots', '*.*'))):
753 if not os.path.isfile(f):
755 segments = f.split('/')
756 packageName = segments[1]
758 screenshotdir = segments[3]
759 filename = os.path.basename(f)
760 base, extension = common.get_extension(filename)
762 if packageName not in apps:
763 logging.warning('Found "%s" graphic without metadata for app "%s"!'
764 % (filename, packageName))
766 graphics = _get_localized_dict(apps[packageName], locale)
768 if extension not in ALLOWED_EXTENSIONS:
769 logging.warning('Only PNG and JPEG are supported for graphics, found: ' + f)
770 elif base in GRAPHIC_NAMES:
771 # there can only be zero or one of these per locale
772 graphics[base] = filename
773 elif screenshotdir in SCREENSHOT_DIRS:
774 # there can any number of these per locale
775 logging.debug('adding ' + base + ':' + f)
776 if screenshotdir not in graphics:
777 graphics[screenshotdir] = []
778 graphics[screenshotdir].append(filename)
780 logging.warning('Unsupported graphics file found: ' + f)
783 def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
784 """Scan a repo for all files with an extension except APK/OBB
786 :param apkcache: current cached info about all repo files
787 :param repodir: repo directory to scan
788 :param knownapks: list of all known files, as per metadata.read_metadata
789 :param use_date_from_file: use date from file (instead of current date)
790 for newly added files
795 repodir = repodir.encode('utf-8')
796 for name in os.listdir(repodir):
797 file_extension = common.get_file_extension(name)
798 if file_extension == 'apk' or file_extension == 'obb':
800 filename = os.path.join(repodir, name)
801 name_utf8 = name.decode('utf-8')
802 if filename.endswith(b'_src.tar.gz'):
803 logging.debug('skipping source tarball: ' + filename.decode('utf-8'))
805 if not common.is_repo_file(filename):
807 stat = os.stat(filename)
808 if stat.st_size == 0:
809 logging.error(filename + ' is zero size!')
812 shasum = sha256sum(filename)
815 repo_file = apkcache[name]
816 # added time is cached as tuple but used here as datetime instance
817 if 'added' in repo_file:
818 a = repo_file['added']
819 if isinstance(a, datetime):
820 repo_file['added'] = a
822 repo_file['added'] = datetime(*a[:6])
823 if repo_file.get('hash') == shasum:
824 logging.debug("Reading " + name_utf8 + " from cache")
827 logging.debug("Ignoring stale cache data for " + name)
830 logging.debug("Processing " + name_utf8)
831 repo_file = collections.OrderedDict()
832 # TODO rename apkname globally to something more generic
833 repo_file['name'] = name_utf8
834 repo_file['apkName'] = name_utf8
835 repo_file['hash'] = shasum
836 repo_file['hashType'] = 'sha256'
837 repo_file['versionCode'] = 0
838 repo_file['versionName'] = shasum
839 # the static ID is the SHA256 unless it is set in the metadata
840 repo_file['packageName'] = shasum
841 n = name_utf8.split('_')
844 versionCode = n[1].split('.')[0]
845 if re.match('^-?[0-9]+$', versionCode) \
846 and common.is_valid_package_name(name_utf8.split('_')[0]):
847 repo_file['packageName'] = packageName
848 repo_file['versionCode'] = int(versionCode)
849 srcfilename = name + b'_src.tar.gz'
850 if os.path.exists(os.path.join(repodir, srcfilename)):
851 repo_file['srcname'] = srcfilename.decode('utf-8')
852 repo_file['size'] = stat.st_size
854 apkcache[name] = repo_file
857 if use_date_from_file:
858 timestamp = stat.st_ctime
859 default_date_param = datetime.fromtimestamp(timestamp).utctimetuple()
861 default_date_param = None
863 # Record in knownapks, getting the added date at the same time..
864 added = knownapks.recordapk(repo_file['apkName'], repo_file['packageName'],
865 default_date=default_date_param)
867 repo_file['added'] = added
869 repo_files.append(repo_file)
871 return repo_files, cachechanged
874 def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
875 """Scan the apk with the given filename in the given repo directory.
877 This also extracts the icons.
879 :param apkcache: current apk cache information
880 :param apkfilename: the filename of the apk to scan
881 :param repodir: repo directory to scan
882 :param knownapks: known apks info
883 :param use_date_from_apk: use date from APK (instead of current date)
885 :returns: (skip, apk, cachechanged) where skip is a boolean indicating whether to skip this apk,
886 apk is the scanned apk information, and cachechanged is True if the apkcache got changed.
889 if ' ' in apkfilename:
890 logging.critical("Spaces in filenames are not allowed.")
893 apkfile = os.path.join(repodir, apkfilename)
894 shasum = sha256sum(apkfile)
898 if apkfilename in apkcache:
899 apk = apkcache[apkfilename]
900 if apk.get('hash') == shasum:
901 logging.debug("Reading " + apkfilename + " from cache")
904 logging.debug("Ignoring stale cache data for " + apkfilename)
907 logging.debug("Processing " + apkfilename)
909 apk['apkName'] = apkfilename
911 apk['hashType'] = 'sha256'
912 srcfilename = apkfilename[:-4] + "_src.tar.gz"
913 if os.path.exists(os.path.join(repodir, srcfilename)):
914 apk['srcname'] = srcfilename
915 apk['size'] = os.path.getsize(apkfile)
916 apk['uses-permission'] = []
917 apk['uses-permission-sdk-23'] = []
919 apk['icons_src'] = {}
921 apk['antiFeatures'] = set()
922 if has_old_openssl(apkfile):
923 apk['antiFeatures'].add('KnownVuln')
924 p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
925 if p.returncode != 0:
926 if options.delete_unknown:
927 if os.path.exists(apkfile):
928 logging.error("Failed to get apk information, deleting " + apkfile)
931 logging.error("Could not find {0} to remove it".format(apkfile))
933 logging.error("Failed to get apk information, skipping " + apkfile)
934 return True, None, False
935 for line in p.output.splitlines():
936 if line.startswith("package:"):
938 apk['packageName'] = re.match(APK_NAME_PAT, line).group(1)
939 apk['versionCode'] = int(re.match(APK_VERCODE_PAT, line).group(1))
940 apk['versionName'] = re.match(APK_VERNAME_PAT, line).group(1)
941 except Exception as e:
942 logging.error("Package matching failed: " + str(e))
943 logging.info("Line was: " + line)
945 elif line.startswith("application:"):
946 apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
947 # Keep path to non-dpi icon in case we need it
948 match = re.match(APK_ICON_PAT_NODPI, line)
950 apk['icons_src']['-1'] = match.group(1)
951 elif line.startswith("launchable-activity:"):
952 # Only use launchable-activity as fallback to application
954 apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
955 if '-1' not in apk['icons_src']:
956 match = re.match(APK_ICON_PAT_NODPI, line)
958 apk['icons_src']['-1'] = match.group(1)
959 elif line.startswith("application-icon-"):
960 match = re.match(APK_ICON_PAT, line)
962 density = match.group(1)
963 path = match.group(2)
964 apk['icons_src'][density] = path
965 elif line.startswith("sdkVersion:"):
966 m = re.match(APK_SDK_VERSION_PAT, line)
968 logging.error(line.replace('sdkVersion:', '')
969 + ' is not a valid minSdkVersion!')
971 apk['minSdkVersion'] = m.group(1)
972 # if target not set, default to min
973 if 'targetSdkVersion' not in apk:
974 apk['targetSdkVersion'] = m.group(1)
975 elif line.startswith("targetSdkVersion:"):
976 m = re.match(APK_SDK_VERSION_PAT, line)
978 logging.error(line.replace('targetSdkVersion:', '')
979 + ' is not a valid targetSdkVersion!')
981 apk['targetSdkVersion'] = m.group(1)
982 elif line.startswith("maxSdkVersion:"):
983 apk['maxSdkVersion'] = re.match(APK_SDK_VERSION_PAT, line).group(1)
984 elif line.startswith("native-code:"):
985 apk['nativecode'] = []
986 for arch in line[13:].split(' '):
987 apk['nativecode'].append(arch[1:-1])
988 elif line.startswith('uses-permission:'):
989 perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
990 if perm_match['maxSdkVersion']:
991 perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
992 permission = UsesPermission(
994 perm_match['maxSdkVersion']
997 apk['uses-permission'].append(permission)
998 elif line.startswith('uses-permission-sdk-23:'):
999 perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
1000 if perm_match['maxSdkVersion']:
1001 perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
1002 permission_sdk_23 = UsesPermissionSdk23(
1004 perm_match['maxSdkVersion']
1007 apk['uses-permission-sdk-23'].append(permission_sdk_23)
1009 elif line.startswith('uses-feature:'):
1010 feature = re.match(APK_FEATURE_PAT, line).group(1)
1011 # Filter out this, it's only added with the latest SDK tools and
1012 # causes problems for lots of apps.
1013 if feature != "android.hardware.screen.portrait" \
1014 and feature != "android.hardware.screen.landscape":
1015 if feature.startswith("android.feature."):
1016 feature = feature[16:]
1017 apk['features'].add(feature)
1019 if 'minSdkVersion' not in apk:
1020 logging.warn("No SDK version information found in {0}".format(apkfile))
1021 apk['minSdkVersion'] = 1
1023 # Check for debuggable apks...
1024 if common.isApkAndDebuggable(apkfile, config):
1025 logging.warning('{0} is set to android:debuggable="true"'.format(apkfile))
1027 # Get the signature (or md5 of, to be precise)...
1028 logging.debug('Getting signature of {0}'.format(apkfile))
1029 apk['sig'] = getsig(os.path.join(os.getcwd(), apkfile))
1031 logging.critical("Failed to get apk signature")
1034 apkzip = zipfile.ZipFile(apkfile, 'r')
1036 # if an APK has files newer than the system time, suggest updating
1037 # the system clock. This is useful for offline systems, used for
1038 # signing, which do not have another source of clock sync info. It
1039 # has to be more than 24 hours newer because ZIP/APK files do not
1040 # store timezone info
1041 manifest = apkzip.getinfo('AndroidManifest.xml')
1042 if manifest.date_time[1] == 0: # month can't be zero
1043 logging.debug('AndroidManifest.xml has no date')
1045 dt_obj = datetime(*manifest.date_time)
1046 checkdt = dt_obj - timedelta(1)
1047 if datetime.today() < checkdt:
1048 logging.warn('System clock is older than manifest in: '
1050 + '\nSet clock to that time using:\n'
1051 + 'sudo date -s "' + str(dt_obj) + '"')
1053 iconfilename = "%s.%s.png" % (
1057 # Extract the icon file...
1058 empty_densities = []
1059 for density in screen_densities:
1060 if density not in apk['icons_src']:
1061 empty_densities.append(density)
1063 iconsrc = apk['icons_src'][density]
1064 icon_dir = get_icon_dir(repodir, density)
1065 icondest = os.path.join(icon_dir, iconfilename)
1068 with open(icondest, 'wb') as f:
1069 f.write(get_icon_bytes(apkzip, iconsrc))
1070 apk['icons'][density] = iconfilename
1072 except Exception as e:
1073 logging.warn("Error retrieving icon file: %s" % (e))
1074 del apk['icons'][density]
1075 del apk['icons_src'][density]
1076 empty_densities.append(density)
1078 if '-1' in apk['icons_src']:
1079 iconsrc = apk['icons_src']['-1']
1080 iconpath = os.path.join(
1081 get_icon_dir(repodir, '0'), iconfilename)
1082 with open(iconpath, 'wb') as f:
1083 f.write(get_icon_bytes(apkzip, iconsrc))
1085 im = Image.open(iconpath)
1086 dpi = px_to_dpi(im.size[0])
1087 for density in screen_densities:
1088 if density in apk['icons']:
1090 if density == screen_densities[-1] or dpi >= int(density):
1091 apk['icons'][density] = iconfilename
1092 shutil.move(iconpath,
1093 os.path.join(get_icon_dir(repodir, density), iconfilename))
1094 empty_densities.remove(density)
1096 except Exception as e:
1097 logging.warn("Failed reading {0} - {1}".format(iconpath, e))
1100 apk['icon'] = iconfilename
1104 # First try resizing down to not lose quality
1106 for density in screen_densities:
1107 if density not in empty_densities:
1108 last_density = density
1110 if last_density is None:
1112 logging.debug("Density %s not available, resizing down from %s"
1113 % (density, last_density))
1115 last_iconpath = os.path.join(
1116 get_icon_dir(repodir, last_density), iconfilename)
1117 iconpath = os.path.join(
1118 get_icon_dir(repodir, density), iconfilename)
1121 fp = open(last_iconpath, 'rb')
1124 size = dpi_to_px(density)
1126 im.thumbnail((size, size), Image.ANTIALIAS)
1127 im.save(iconpath, "PNG")
1128 empty_densities.remove(density)
1129 except Exception as e:
1130 logging.warning("Invalid image file at %s: %s" % (last_iconpath, e))
1135 # Then just copy from the highest resolution available
1137 for density in reversed(screen_densities):
1138 if density not in empty_densities:
1139 last_density = density
1141 if last_density is None:
1143 logging.debug("Density %s not available, copying from lower density %s"
1144 % (density, last_density))
1147 os.path.join(get_icon_dir(repodir, last_density), iconfilename),
1148 os.path.join(get_icon_dir(repodir, density), iconfilename))
1150 empty_densities.remove(density)
1152 for density in screen_densities:
1153 icon_dir = get_icon_dir(repodir, density)
1154 icondest = os.path.join(icon_dir, iconfilename)
1155 resize_icon(icondest, density)
1157 # Copy from icons-mdpi to icons since mdpi is the baseline density
1158 baseline = os.path.join(get_icon_dir(repodir, '160'), iconfilename)
1159 if os.path.isfile(baseline):
1160 apk['icons']['0'] = iconfilename
1161 shutil.copyfile(baseline,
1162 os.path.join(get_icon_dir(repodir, '0'), iconfilename))
1164 if use_date_from_apk and manifest.date_time[1] != 0:
1165 default_date_param = datetime(*manifest.date_time)
1167 default_date_param = None
1169 # Record in known apks, getting the added date at the same time..
1170 added = knownapks.recordapk(apk['apkName'], apk['packageName'],
1171 default_date=default_date_param)
1173 apk['added'] = added
1175 apkcache[apkfilename] = apk
1178 return False, apk, cachechanged
1181 def scan_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
1182 """Scan the apks in the given repo directory.
1184 This also extracts the icons.
1186 :param apkcache: current apk cache information
1187 :param repodir: repo directory to scan
1188 :param knownapks: known apks info
1189 :param use_date_from_apk: use date from APK (instead of current date)
1190 for newly added APKs
1191 :returns: (apks, cachechanged) where apks is a list of apk information,
1192 and cachechanged is True if the apkcache got changed.
1195 cachechanged = False
1197 for icon_dir in get_all_icon_dirs(repodir):
1198 if os.path.exists(icon_dir):
1200 shutil.rmtree(icon_dir)
1201 os.makedirs(icon_dir)
1203 os.makedirs(icon_dir)
1206 for apkfile in sorted(glob.glob(os.path.join(repodir, '*.apk'))):
1207 apkfilename = apkfile[len(repodir) + 1:]
1208 (skip, apk, cachechanged) = scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk)
1213 return apks, cachechanged
1216 def apply_info_from_latest_apk(apps, apks):
1218 Some information from the apks needs to be applied up to the application level.
1219 When doing this, we use the info from the most recent version's apk.
1220 We deal with figuring out when the app was added and last updated at the same time.
1222 for appid, app in apps.items():
1223 bestver = UNSET_VERSION_CODE
1225 if apk['packageName'] == appid:
1226 if apk['versionCode'] > bestver:
1227 bestver = apk['versionCode']
1231 if not app.added or apk['added'] < app.added:
1232 app.added = apk['added']
1233 if not app.lastUpdated or apk['added'] > app.lastUpdated:
1234 app.lastUpdated = apk['added']
1237 logging.debug("Don't know when " + appid + " was added")
1238 if not app.lastUpdated:
1239 logging.debug("Don't know when " + appid + " was last updated")
1241 if bestver == UNSET_VERSION_CODE:
1243 if app.Name is None:
1244 app.Name = app.AutoName or appid
1246 logging.debug("Application " + appid + " has no packages")
1248 if app.Name is None:
1249 app.Name = bestapk['name']
1250 app.icon = bestapk['icon'] if 'icon' in bestapk else None
1251 if app.CurrentVersionCode is None:
1252 app.CurrentVersionCode = str(bestver)
1255 def make_categories_txt(repodir, categories):
1256 '''Write a category list in the repo to allow quick access'''
1258 for cat in sorted(categories):
1259 catdata += cat + '\n'
1260 with open(os.path.join(repodir, 'categories.txt'), 'w', encoding='utf8') as f:
1264 def archive_old_apks(apps, apks, archapks, repodir, archivedir, defaultkeepversions):
1266 for appid, app in apps.items():
1268 if app.ArchivePolicy:
1269 keepversions = int(app.ArchivePolicy[:-9])
1271 keepversions = defaultkeepversions
1273 def filter_apk_list_sorted(apk_list):
1275 for apk in apk_list:
1276 if apk['packageName'] == appid:
1279 # Sort the apk list by version code. First is highest/newest.
1280 return sorted(res, key=lambda apk: apk['versionCode'], reverse=True)
1282 def move_file(from_dir, to_dir, filename, ignore_missing):
1283 from_path = os.path.join(from_dir, filename)
1284 if ignore_missing and not os.path.exists(from_path):
1286 to_path = os.path.join(to_dir, filename)
1287 shutil.move(from_path, to_path)
1289 logging.debug("Checking archiving for {0} - apks:{1}, keepversions:{2}, archapks:{3}"
1290 .format(appid, len(apks), keepversions, len(archapks)))
1292 if len(apks) > keepversions:
1293 apklist = filter_apk_list_sorted(apks)
1294 # Move back the ones we don't want.
1295 for apk in apklist[keepversions:]:
1296 logging.info("Moving " + apk['apkName'] + " to archive")
1297 move_file(repodir, archivedir, apk['apkName'], False)
1298 move_file(repodir, archivedir, apk['apkName'] + '.asc', True)
1299 for density in all_screen_densities:
1300 repo_icon_dir = get_icon_dir(repodir, density)
1301 archive_icon_dir = get_icon_dir(archivedir, density)
1302 if density not in apk['icons']:
1304 move_file(repo_icon_dir, archive_icon_dir, apk['icons'][density], True)
1305 if 'srcname' in apk:
1306 move_file(repodir, archivedir, apk['srcname'], False)
1307 archapks.append(apk)
1309 elif len(apks) < keepversions and len(archapks) > 0:
1310 required = keepversions - len(apks)
1311 archapklist = filter_apk_list_sorted(archapks)
1312 # Move forward the ones we want again.
1313 for apk in archapklist[:required]:
1314 logging.info("Moving " + apk['apkName'] + " from archive")
1315 move_file(archivedir, repodir, apk['apkName'], False)
1316 move_file(archivedir, repodir, apk['apkName'] + '.asc', True)
1317 for density in all_screen_densities:
1318 repo_icon_dir = get_icon_dir(repodir, density)
1319 archive_icon_dir = get_icon_dir(archivedir, density)
1320 if density not in apk['icons']:
1322 move_file(archive_icon_dir, repo_icon_dir, apk['icons'][density], True)
1323 if 'srcname' in apk:
1324 move_file(archivedir, repodir, apk['srcname'], False)
1325 archapks.remove(apk)
1329 def add_apks_to_per_app_repos(repodir, apks):
1330 apks_per_app = dict()
1332 apk['per_app_dir'] = os.path.join(apk['packageName'], 'fdroid')
1333 apk['per_app_repo'] = os.path.join(apk['per_app_dir'], 'repo')
1334 apk['per_app_icons'] = os.path.join(apk['per_app_repo'], 'icons')
1335 apks_per_app[apk['packageName']] = apk
1337 if not os.path.exists(apk['per_app_icons']):
1338 logging.info('Adding new repo for only ' + apk['packageName'])
1339 os.makedirs(apk['per_app_icons'])
1341 apkpath = os.path.join(repodir, apk['apkName'])
1342 shutil.copy(apkpath, apk['per_app_repo'])
1343 apksigpath = apkpath + '.sig'
1344 if os.path.exists(apksigpath):
1345 shutil.copy(apksigpath, apk['per_app_repo'])
1346 apkascpath = apkpath + '.asc'
1347 if os.path.exists(apkascpath):
1348 shutil.copy(apkascpath, apk['per_app_repo'])
1357 global config, options
1359 # Parse command line...
1360 parser = ArgumentParser()
1361 common.setup_global_opts(parser)
1362 parser.add_argument("--create-key", action="store_true", default=False,
1363 help="Create a repo signing key in a keystore")
1364 parser.add_argument("-c", "--create-metadata", action="store_true", default=False,
1365 help="Create skeleton metadata files that are missing")
1366 parser.add_argument("--delete-unknown", action="store_true", default=False,
1367 help="Delete APKs and/or OBBs without metadata from the repo")
1368 parser.add_argument("-b", "--buildreport", action="store_true", default=False,
1369 help="Report on build data status")
1370 parser.add_argument("-i", "--interactive", default=False, action="store_true",
1371 help="Interactively ask about things that need updating.")
1372 parser.add_argument("-I", "--icons", action="store_true", default=False,
1373 help="Resize all the icons exceeding the max pixel size and exit")
1374 parser.add_argument("-e", "--editor", default="/etc/alternatives/editor",
1375 help="Specify editor to use in interactive mode. Default " +
1376 "is /etc/alternatives/editor")
1377 parser.add_argument("-w", "--wiki", default=False, action="store_true",
1378 help="Update the wiki")
1379 parser.add_argument("--pretty", action="store_true", default=False,
1380 help="Produce human-readable index.xml")
1381 parser.add_argument("--clean", action="store_true", default=False,
1382 help="Clean update - don't uses caches, reprocess all apks")
1383 parser.add_argument("--nosign", action="store_true", default=False,
1384 help="When configured for signed indexes, create only unsigned indexes at this stage")
1385 parser.add_argument("--use-date-from-apk", action="store_true", default=False,
1386 help="Use date from apk instead of current time for newly added apks")
1387 metadata.add_metadata_arguments(parser)
1388 options = parser.parse_args()
1389 metadata.warnings_action = options.W
1391 config = common.read_config(options)
1393 if not ('jarsigner' in config and 'keytool' in config):
1394 logging.critical('Java JDK not found! Install in standard location or set java_paths!')
1398 if config['archive_older'] != 0:
1399 repodirs.append('archive')
1400 if not os.path.exists('archive'):
1404 resize_all_icons(repodirs)
1407 # check that icons exist now, rather than fail at the end of `fdroid update`
1408 for k in ['repo_icon', 'archive_icon']:
1410 if not os.path.exists(config[k]):
1411 logging.critical(k + ' "' + config[k] + '" does not exist! Correct it in config.py.')
1414 # if the user asks to create a keystore, do it now, reusing whatever it can
1415 if options.create_key:
1416 if os.path.exists(config['keystore']):
1417 logging.critical("Cowardily refusing to overwrite existing signing key setup!")
1418 logging.critical("\t'" + config['keystore'] + "'")
1421 if 'repo_keyalias' not in config:
1422 config['repo_keyalias'] = socket.getfqdn()
1423 common.write_to_config(config, 'repo_keyalias', config['repo_keyalias'])
1424 if 'keydname' not in config:
1425 config['keydname'] = 'CN=' + config['repo_keyalias'] + ', OU=F-Droid'
1426 common.write_to_config(config, 'keydname', config['keydname'])
1427 if 'keystore' not in config:
1428 config['keystore'] = common.default_config.keystore
1429 common.write_to_config(config, 'keystore', config['keystore'])
1431 password = common.genpassword()
1432 if 'keystorepass' not in config:
1433 config['keystorepass'] = password
1434 common.write_to_config(config, 'keystorepass', config['keystorepass'])
1435 if 'keypass' not in config:
1436 config['keypass'] = password
1437 common.write_to_config(config, 'keypass', config['keypass'])
1438 common.genkeystore(config)
1441 apps = metadata.read_metadata()
1443 # Generate a list of categories...
1445 for app in apps.values():
1446 categories.update(app.Categories)
1448 # Read known apks data (will be updated and written back when we've finished)
1449 knownapks = common.KnownApks()
1452 apkcache = get_cache()
1454 # Delete builds for disabled apps
1455 delete_disabled_builds(apps, apkcache, repodirs)
1457 # Scan all apks in the main repo
1458 apks, cachechanged = scan_apks(apkcache, repodirs[0], knownapks, options.use_date_from_apk)
1460 files, fcachechanged = scan_repo_files(apkcache, repodirs[0], knownapks,
1461 options.use_date_from_apk)
1462 cachechanged = cachechanged or fcachechanged
1464 # Generate warnings for apk's with no metadata (or create skeleton
1465 # metadata files, if requested on the command line)
1468 if apk['packageName'] not in apps:
1469 if options.create_metadata:
1470 if 'name' not in apk:
1471 logging.error(apk['packageName'] + ' does not have a name! Skipping...')
1473 f = open(os.path.join('metadata', apk['packageName'] + '.txt'), 'w', encoding='utf8')
1474 f.write("License:Unknown\n")
1475 f.write("Web Site:\n")
1476 f.write("Source Code:\n")
1477 f.write("Issue Tracker:\n")
1478 f.write("Changelog:\n")
1479 f.write("Summary:" + apk['name'] + "\n")
1480 f.write("Description:\n")
1481 f.write(apk['name'] + "\n")
1483 f.write("Name:" + apk['name'] + "\n")
1485 logging.info("Generated skeleton metadata for " + apk['packageName'])
1488 msg = apk['apkName'] + " (" + apk['packageName'] + ") has no metadata!"
1489 if options.delete_unknown:
1490 logging.warn(msg + "\n\tdeleting: repo/" + apk['apkName'])
1491 rmf = os.path.join(repodirs[0], apk['apkName'])
1492 if not os.path.exists(rmf):
1493 logging.error("Could not find {0} to remove it".format(rmf))
1497 logging.warn(msg + "\n\tUse `fdroid update -c` to create it.")
1499 # update the metadata with the newly created ones included
1501 apps = metadata.read_metadata()
1503 copy_triple_t_store_metadata(apps)
1504 insert_obbs(repodirs[0], apps, apks)
1505 insert_localized_app_metadata(apps)
1507 # Scan the archive repo for apks as well
1508 if len(repodirs) > 1:
1509 archapks, cc = scan_apks(apkcache, repodirs[1], knownapks, options.use_date_from_apk)
1515 # Apply information from latest apks to the application and update dates
1516 apply_info_from_latest_apk(apps, apks + archapks)
1518 # Sort the app list by name, then the web site doesn't have to by default.
1519 # (we had to wait until we'd scanned the apks to do this, because mostly the
1520 # name comes from there!)
1521 sortedids = sorted(apps.keys(), key=lambda appid: apps[appid].Name.upper())
1523 # APKs are placed into multiple repos based on the app package, providing
1524 # per-app subscription feeds for nightly builds and things like it
1525 if config['per_app_repos']:
1526 add_apks_to_per_app_repos(repodirs[0], apks)
1527 for appid, app in apps.items():
1528 repodir = os.path.join(appid, 'fdroid', 'repo')
1530 appdict[appid] = app
1531 if os.path.isdir(repodir):
1532 index.make(appdict, [appid], apks, repodir, False)
1534 logging.info('Skipping index generation for ' + appid)
1537 if len(repodirs) > 1:
1538 archive_old_apks(apps, apks, archapks, repodirs[0], repodirs[1], config['archive_older'])
1540 # Make the index for the main repo...
1541 index.make(apps, sortedids, apks, repodirs[0], False)
1542 make_categories_txt(repodirs[0], categories)
1544 # If there's an archive repo, make the index for it. We already scanned it
1546 if len(repodirs) > 1:
1547 index.make(apps, sortedids, archapks, repodirs[1], True)
1549 git_remote = config.get('binary_transparency_remote')
1550 if git_remote or os.path.isdir(os.path.join('binary_transparency', '.git')):
1551 btlog.make_binary_transparency_log(repodirs)
1553 if config['update_stats']:
1554 # Update known apks info...
1555 knownapks.writeifchanged()
1557 # Generate latest apps data for widget
1558 if os.path.exists(os.path.join('stats', 'latestapps.txt')):
1560 with open(os.path.join('stats', 'latestapps.txt'), 'r', encoding='utf8') as f:
1562 appid = line.rstrip()
1563 data += appid + "\t"
1565 data += app.Name + "\t"
1566 if app.icon is not None:
1567 data += app.icon + "\t"
1568 data += app.License + "\n"
1569 with open(os.path.join(repodirs[0], 'latestapps.dat'), 'w', encoding='utf8') as f:
1573 write_cache(apkcache)
1575 # Update the wiki...
1577 update_wiki(apps, sortedids, apks + archapks)
1579 logging.info("Finished.")
1582 if __name__ == "__main__":