2 # -*- coding: utf-8 -*-
4 # checkupdates.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013-2014 Daniel Martà <mvdan@mvdan.cc>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 from optparse import OptionParser
30 from distutils.version import LooseVersion
33 import common, metadata
34 from common import BuildException
35 from common import VCSException
36 from metadata import MetaDataException
39 # Check for a new version by looking at a document retrieved via HTTP.
40 # The app's Update Check Data field is used to provide the information
46 if not 'Update Check Data' in app:
47 raise Exception('Missing Update Check Data')
49 urlcode, codeex, urlver, verex = app['Update Check Data'].split('|')
53 logging.debug("...requesting {0}".format(urlcode))
54 req = urllib2.Request(urlcode, None)
55 resp = urllib2.urlopen(req, None, 20)
58 m = re.search(codeex, page)
60 raise Exception("No RE match for version code")
66 logging.debug("...requesting {0}".format(urlver))
67 req = urllib2.Request(urlver, None)
68 resp = urllib2.urlopen(req, None, 20)
71 m = re.search(verex, page)
73 raise Exception("No RE match for version")
76 return (version, vercode)
79 msg = "Could not complete http check for app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
82 # Check for a new version by looking at the tags in the source repo.
83 # Whether this can be used reliably or not depends on
84 # the development procedures used by the project's developers. Use it with
85 # caution, because it's inappropriate for many projects.
86 # Returns (None, "a message") if this didn't work, or (version, vercode) for
87 # the details of the current version.
88 def check_tags(app, pattern):
92 appid = app['Update Check Name'] if app['Update Check Name'] else app['id']
93 if app['Repo Type'] == 'srclib':
94 build_dir = os.path.join('build', 'srclib', app['Repo'])
95 repotype = common.getsrclibvcs(app['Repo'])
97 build_dir = os.path.join('build/', app['id'])
98 repotype = app['Repo Type']
100 if repotype not in ('git', 'git-svn', 'hg', 'bzr'):
101 return (None, 'Tags update mode only works for git, hg, bzr and git-svn repositories currently', None)
103 # Set up vcs interface and make sure we have the latest code...
104 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
106 vcs.gotorevision(None)
109 if len(app['builds']) > 0:
110 if 'subdir' in app['builds'][-1]:
111 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
112 if 'gradle' in app['builds'][-1]:
113 flavour = app['builds'][-1]['gradle']
123 pat = re.compile(pattern)
124 tags = [tag for tag in tags if pat.match(tag)]
126 if repotype in ('git',):
127 tags = vcs.latesttags(tags, 5)
130 logging.debug("Check tag: '{0}'".format(tag))
131 vcs.gotorevision(tag)
133 # Only process tags where the manifest exists...
134 paths = common.manifest_paths(build_dir, flavour)
135 version, vercode, package = common.parse_androidmanifests(paths)
136 if not package or package != appid or not version or not vercode:
139 logging.debug("Manifest exists. Found version {0} ({1})".format(
141 if int(vercode) > int(hcode):
143 hcode = str(int(vercode))
147 return (hver, hcode, htag)
148 return (None, "Couldn't find any version information", None)
150 except BuildException as be:
151 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
152 return (None, msg, None)
153 except VCSException as vcse:
154 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
155 return (None, msg, None)
157 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
158 return (None, msg, None)
160 # Check for a new version by looking at the AndroidManifest.xml at the HEAD
161 # of the source repo. Whether this can be used reliably or not depends on
162 # the development procedures used by the project's developers. Use it with
163 # caution, because it's inappropriate for many projects.
164 # Returns (None, "a message") if this didn't work, or (version, vercode) for
165 # the details of the current version.
166 def check_repomanifest(app, branch=None):
170 appid = app['Update Check Name'] if app['Update Check Name'] else app['id']
171 if app['Repo Type'] == 'srclib':
172 build_dir = os.path.join('build', 'srclib', app['Repo'])
173 repotype = common.getsrclibvcs(app['Repo'])
175 build_dir = os.path.join('build/', app['id'])
176 repotype = app['Repo Type']
178 # Set up vcs interface and make sure we have the latest code...
179 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
181 if repotype == 'git':
183 branch = 'origin/'+branch
184 vcs.gotorevision(branch)
185 elif repotype == 'git-svn':
186 vcs.gotorevision(branch)
187 elif repotype == 'svn':
188 vcs.gotorevision(None)
189 elif repotype == 'hg':
190 vcs.gotorevision(branch)
191 elif repotype == 'bzr':
192 vcs.gotorevision(None)
196 if len(app['builds']) > 0:
197 if 'subdir' in app['builds'][-1]:
198 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
199 if 'gradle' in app['builds'][-1]:
200 flavour = app['builds'][-1]['gradle']
204 if not os.path.isdir(build_dir):
205 return (None, "Subdir '" + app['builds'][-1]['subdir'] + "'is not a valid directory")
207 paths = common.manifest_paths(build_dir, flavour)
209 version, vercode, package = common.parse_androidmanifests(paths)
211 return (None, "Couldn't find package ID")
213 return (None, "Package ID mismatch")
215 return (None, "Couldn't find latest version name")
217 return (None, "Couldn't find latest version code")
219 vercode = str(int(vercode))
221 logging.debug("Manifest exists. Found version {0} ({1})".format(version, vercode))
223 return (version, vercode)
225 except BuildException as be:
226 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
228 except VCSException as vcse:
229 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
232 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
235 def check_repotrunk(app, branch=None):
238 if app['Repo Type'] == 'srclib':
239 build_dir = os.path.join('build', 'srclib', app['Repo'])
240 repotype = common.getsrclibvcs(app['Repo'])
242 build_dir = os.path.join('build/', app['id'])
243 repotype = app['Repo Type']
245 if repotype not in ('svn', 'git-svn'):
246 return (None, 'RepoTrunk update mode only makes sense in svn and git-svn repositories')
248 # Set up vcs interface and make sure we have the latest code...
249 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
251 vcs.gotorevision(None)
255 except BuildException as be:
256 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
258 except VCSException as vcse:
259 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
262 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
265 # Check for a new version by looking at the Google Play Store.
266 # Returns (None, "a message") if this didn't work, or (version, None) for
267 # the details of the current version.
268 def check_gplay(app):
270 url = 'https://play.google.com/store/apps/details?id=' + app['id']
271 headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux i686; rv:18.0) Gecko/20100101 Firefox/18.0'}
272 req = urllib2.Request(url, None, headers)
274 resp = urllib2.urlopen(req, None, 20)
276 except urllib2.HTTPError, e:
277 return (None, str(e.code))
279 return (None, 'Failed:' + str(e))
283 m = re.search('itemprop="softwareVersion">[ ]*([^<]+)[ ]*</div>', page)
285 html_parser = HTMLParser.HTMLParser()
286 version = html_parser.unescape(m.group(1))
288 if version == 'Varies with device':
289 return (None, 'Device-variable version, cannot use this method')
292 return (None, "Couldn't find version")
293 return (version.strip(), None)
301 global config, options
303 # Parse command line...
304 parser = OptionParser(usage="Usage: %prog [options] [APPID [APPID ...]]")
305 parser.add_option("-v", "--verbose", action="store_true", default=False,
306 help="Spew out even more information than normal")
307 parser.add_option("-q", "--quiet", action="store_true", default=False,
308 help="Restrict output to warnings and errors")
309 parser.add_option("--auto", action="store_true", default=False,
310 help="Process auto-updates")
311 parser.add_option("--autoonly", action="store_true", default=False,
312 help="Only process apps with auto-updates")
313 parser.add_option("--commit", action="store_true", default=False,
314 help="Commit changes")
315 parser.add_option("--gplay", action="store_true", default=False,
316 help="Only print differences with the Play Store")
317 (options, args) = parser.parse_args()
319 config = common.read_config(options)
322 allapps = metadata.read_metadata(options.verbose)
324 apps = common.read_app_args(args, allapps, False)
328 version, reason = check_gplay(app)
331 logging.info("{0} is not in the Play Store".format(common.getappname(app)))
333 logging.info("{0} encountered a problem: {1}".format(common.getappname(app), reason))
334 if version is not None:
335 stored = app['Current Version']
337 logging.info("{0} has no Current Version but has version {1} on the Play Store".format(
338 common.getappname(app), version))
339 elif LooseVersion(stored) < LooseVersion(version):
340 logging.info("{0} has version {1} on the Play Store, which is bigger than {2}".format(
341 common.getappname(app), version, stored))
343 if stored != version:
344 logging.info("{0} has version {1} on the Play Store, which differs from {2}".format(
345 common.getappname(app), version, stored))
347 logging.info("{0} has the same version {1} on the Play Store".format(
348 common.getappname(app), version))
354 if options.autoonly and app['Auto Update Mode'] in ('None', 'Static'):
355 logging.debug("Nothing to do for {0}...".format(app['id']))
358 logging.info("Processing " + app['id'] + '...')
360 # If a change is made, commitmsg should be set to a description of it.
361 # Only if this is set will changes be written back to the metadata.
368 mode = app['Update Check Mode']
369 if mode.startswith('Tags'):
370 pattern = mode[5:] if len(mode) > 4 else None
371 (version, vercode, tag) = check_tags(app, pattern)
373 elif mode == 'RepoManifest':
374 (version, vercode) = check_repomanifest(app)
376 elif mode.startswith('RepoManifest/'):
378 (version, vercode) = check_repomanifest(app, tag)
380 elif mode == 'RepoTrunk':
381 (version, vercode) = check_repotrunk(app)
384 (version, vercode) = check_http(app)
386 elif mode in ('None', 'Static'):
388 msg = 'Checking disabled'
392 msg = 'Invalid update check method'
394 if vercode and app['Vercode Operation']:
395 op = app['Vercode Operation'].replace("%c", str(int(vercode)))
396 vercode = str(eval(op))
400 logmsg = "...{0} : {1}".format(app['id'], msg)
405 elif vercode == app['Current Version Code']:
406 logging.info("...up to date")
408 app['Current Version'] = version
409 app['Current Version Code'] = str(int(vercode))
412 # Do the Auto Name thing as well as finding the CV real name
413 if len(app["Repo Type"]) > 0 and mode not in ('None', 'Static'):
417 if app['Repo Type'] == 'srclib':
418 app_dir = os.path.join('build', 'srclib', app['Repo'])
420 app_dir = os.path.join('build/', app['id'])
422 vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
423 vcs.gotorevision(tag)
426 if len(app['builds']) > 0:
427 if 'subdir' in app['builds'][-1]:
428 app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
429 if 'gradle' in app['builds'][-1]:
430 flavour = app['builds'][-1]['gradle']
434 logging.debug("...fetch auto name from " + app_dir +
435 ((" (flavour: %s)" % flavour) if flavour else ""))
436 new_name = common.fetch_real_name(app_dir, flavour)
438 logging.debug("...got autoname '" + new_name + "'")
439 if new_name != app['Auto Name']:
440 app['Auto Name'] = new_name
442 commitmsg = "Set autoname of {0}".format(common.getappname(app))
444 logging.debug("...couldn't get autoname")
446 if app['Current Version'].startswith('@string/'):
447 cv = common.version_name(app['Current Version'], app_dir, flavour)
448 if app['Current Version'] != cv:
449 app['Current Version'] = cv
451 commitmsg = "Fix CV of {0}".format(common.getappname(app))
453 logging.error("Auto Name or Current Version failed for {0} due to exception: {1}".format(app['id'], traceback.format_exc()))
456 name = common.getappname(app)
457 ver = common.getcvname(app)
458 logging.info('...updating to version %s' % ver)
459 commitmsg = 'Update CV of %s to %s' % (name, ver)
462 mode = app['Auto Update Mode']
463 if mode in ('None', 'Static'):
465 elif mode.startswith('Version '):
467 if pattern.startswith('+'):
469 suffix, pattern = pattern.split(' ', 1)
471 raise MetaDataException("Invalid AUM: " + mode)
476 for build in app['builds']:
477 if build['vercode'] == app['Current Version Code']:
479 if not latest or int(build['vercode']) > int(latest['vercode']):
483 newbuild = latest.copy()
484 for k in ('origlines', 'disable'):
487 newbuild['vercode'] = app['Current Version Code']
488 newbuild['version'] = app['Current Version'] + suffix
489 logging.info("...auto-generating build for " + newbuild['version'])
490 commit = pattern.replace('%v', newbuild['version'])
491 commit = commit.replace('%c', newbuild['vercode'])
492 newbuild['commit'] = commit
493 app['builds'].append(newbuild)
494 name = common.getappname(app)
495 ver = common.getcvname(app)
496 commitmsg = "Update %s to %s" % (name, ver)
498 logging.warn('Invalid auto update mode "' + mode + '" on ' + app['id'])
501 metafile = os.path.join('metadata', app['id'] + '.txt')
502 metadata.write_metadata(metafile, app)
504 logging.info("Commiting update for " + metafile)
505 gitcmd = ["git", "commit", "-m",
507 if 'auto_author' in config:
508 gitcmd.extend(['--author', config['auto_author']])
509 gitcmd.extend(["--", metafile])
510 if subprocess.call(gitcmd) != 0:
511 logging.error("Git commit failed")
514 logging.info("Finished.")
516 if __name__ == "__main__":