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
35 from common import BuildException
36 from common import VCSException
37 from metadata import MetaDataException
40 # Check for a new version by looking at a document retrieved via HTTP.
41 # The app's Update Check Data field is used to provide the information
47 if not 'Update Check Data' in app:
48 raise Exception('Missing Update Check Data')
50 urlcode, codeex, urlver, verex = app['Update Check Data'].split('|')
54 logging.debug("...requesting {0}".format(urlcode))
55 req = urllib2.Request(urlcode, None)
56 resp = urllib2.urlopen(req, None, 20)
59 m = re.search(codeex, page)
61 raise Exception("No RE match for version code")
67 logging.debug("...requesting {0}".format(urlver))
68 req = urllib2.Request(urlver, None)
69 resp = urllib2.urlopen(req, None, 20)
72 m = re.search(verex, page)
74 raise Exception("No RE match for version")
77 return (version, vercode)
80 msg = "Could not complete http check for app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
83 # Check for a new version by looking at the tags in the source repo.
84 # Whether this can be used reliably or not depends on
85 # the development procedures used by the project's developers. Use it with
86 # caution, because it's inappropriate for many projects.
87 # Returns (None, "a message") if this didn't work, or (version, vercode) for
88 # the details of the current version.
89 def check_tags(app, pattern):
93 appid = app['Update Check Name'] if app['Update Check Name'] else app['id']
94 if app['Repo Type'] == 'srclib':
95 build_dir = os.path.join('build', 'srclib', app['Repo'])
96 repotype = common.getsrclibvcs(app['Repo'])
98 build_dir = os.path.join('build/', app['id'])
99 repotype = app['Repo Type']
101 if repotype not in ('git', 'git-svn', 'hg', 'bzr'):
102 return (None, 'Tags update mode only works for git, hg, bzr and git-svn repositories currently', None)
104 # Set up vcs interface and make sure we have the latest code...
105 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
107 vcs.gotorevision(None)
110 if len(app['builds']) > 0:
111 if 'subdir' in app['builds'][-1]:
112 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
113 if 'gradle' in app['builds'][-1]:
114 flavour = app['builds'][-1]['gradle']
124 pat = re.compile(pattern)
125 tags = [tag for tag in tags if pat.match(tag)]
127 if repotype in ('git',):
128 tags = vcs.latesttags(tags, 5)
131 logging.debug("Check tag: '{0}'".format(tag))
132 vcs.gotorevision(tag)
134 # Only process tags where the manifest exists...
135 paths = common.manifest_paths(build_dir, flavour)
136 version, vercode, package = common.parse_androidmanifests(paths)
137 if not package or package != appid or not version or not vercode:
140 logging.debug("Manifest exists. Found version {0} ({1})".format(
142 if int(vercode) > int(hcode):
144 hcode = str(int(vercode))
148 return (hver, hcode, htag)
149 return (None, "Couldn't find any version information", None)
151 except BuildException as be:
152 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
153 return (None, msg, None)
154 except VCSException as vcse:
155 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
156 return (None, msg, None)
158 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
159 return (None, msg, None)
161 # Check for a new version by looking at the AndroidManifest.xml at the HEAD
162 # of the source repo. Whether this can be used reliably or not depends on
163 # the development procedures used by the project's developers. Use it with
164 # caution, because it's inappropriate for many projects.
165 # Returns (None, "a message") if this didn't work, or (version, vercode) for
166 # the details of the current version.
167 def check_repomanifest(app, branch=None):
171 appid = app['Update Check Name'] if app['Update Check Name'] else app['id']
172 if app['Repo Type'] == 'srclib':
173 build_dir = os.path.join('build', 'srclib', app['Repo'])
174 repotype = common.getsrclibvcs(app['Repo'])
176 build_dir = os.path.join('build/', app['id'])
177 repotype = app['Repo Type']
179 # Set up vcs interface and make sure we have the latest code...
180 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
182 if repotype == 'git':
184 branch = 'origin/'+branch
185 vcs.gotorevision(branch)
186 elif repotype == 'git-svn':
187 vcs.gotorevision(branch)
188 elif repotype == 'svn':
189 vcs.gotorevision(None)
190 elif repotype == 'hg':
191 vcs.gotorevision(branch)
192 elif repotype == 'bzr':
193 vcs.gotorevision(None)
197 if len(app['builds']) > 0:
198 if 'subdir' in app['builds'][-1]:
199 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
200 if 'gradle' in app['builds'][-1]:
201 flavour = app['builds'][-1]['gradle']
205 if not os.path.isdir(build_dir):
206 return (None, "Subdir '" + app['builds'][-1]['subdir'] + "'is not a valid directory")
208 paths = common.manifest_paths(build_dir, flavour)
210 version, vercode, package = common.parse_androidmanifests(paths)
212 return (None, "Couldn't find package ID")
214 return (None, "Package ID mismatch")
216 return (None, "Couldn't find latest version name")
218 return (None, "Couldn't find latest version code")
220 vercode = str(int(vercode))
222 logging.debug("Manifest exists. Found version {0} ({1})".format(version, vercode))
224 return (version, vercode)
226 except BuildException as be:
227 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
229 except VCSException as vcse:
230 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
233 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
236 def check_repotrunk(app, branch=None):
239 if app['Repo Type'] == 'srclib':
240 build_dir = os.path.join('build', 'srclib', app['Repo'])
241 repotype = common.getsrclibvcs(app['Repo'])
243 build_dir = os.path.join('build/', app['id'])
244 repotype = app['Repo Type']
246 if repotype not in ('svn', 'git-svn'):
247 return (None, 'RepoTrunk update mode only makes sense in svn and git-svn repositories')
249 # Set up vcs interface and make sure we have the latest code...
250 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
252 vcs.gotorevision(None)
256 except BuildException as be:
257 msg = "Could not scan app {0} due to BuildException: {1}".format(app['id'], be)
259 except VCSException as vcse:
260 msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
263 msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
266 # Check for a new version by looking at the Google Play Store.
267 # Returns (None, "a message") if this didn't work, or (version, None) for
268 # the details of the current version.
269 def check_gplay(app):
271 url = 'https://play.google.com/store/apps/details?id=' + app['id']
272 headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux i686; rv:18.0) Gecko/20100101 Firefox/18.0'}
273 req = urllib2.Request(url, None, headers)
275 resp = urllib2.urlopen(req, None, 20)
277 except urllib2.HTTPError, e:
278 return (None, str(e.code))
280 return (None, 'Failed:' + str(e))
284 m = re.search('itemprop="softwareVersion">[ ]*([^<]+)[ ]*</div>', page)
286 html_parser = HTMLParser.HTMLParser()
287 version = html_parser.unescape(m.group(1))
289 if version == 'Varies with device':
290 return (None, 'Device-variable version, cannot use this method')
293 return (None, "Couldn't find version")
294 return (version.strip(), None)
302 global config, options
304 # Parse command line...
305 parser = OptionParser(usage="Usage: %prog [options] [APPID [APPID ...]]")
306 parser.add_option("-v", "--verbose", action="store_true", default=False,
307 help="Spew out even more information than normal")
308 parser.add_option("-q", "--quiet", action="store_true", default=False,
309 help="Restrict output to warnings and errors")
310 parser.add_option("--auto", action="store_true", default=False,
311 help="Process auto-updates")
312 parser.add_option("--autoonly", action="store_true", default=False,
313 help="Only process apps with auto-updates")
314 parser.add_option("--commit", action="store_true", default=False,
315 help="Commit changes")
316 parser.add_option("--gplay", action="store_true", default=False,
317 help="Only print differences with the Play Store")
318 (options, args) = parser.parse_args()
320 config = common.read_config(options)
323 allapps = metadata.read_metadata(options.verbose)
325 apps = common.read_app_args(args, allapps, False)
329 version, reason = check_gplay(app)
332 logging.info("{0} is not in the Play Store".format(common.getappname(app)))
334 logging.info("{0} encountered a problem: {1}".format(common.getappname(app), reason))
335 if version is not None:
336 stored = app['Current Version']
338 logging.info("{0} has no Current Version but has version {1} on the Play Store".format(
339 common.getappname(app), version))
340 elif LooseVersion(stored) < LooseVersion(version):
341 logging.info("{0} has version {1} on the Play Store, which is bigger than {2}".format(
342 common.getappname(app), version, stored))
344 if stored != version:
345 logging.info("{0} has version {1} on the Play Store, which differs from {2}".format(
346 common.getappname(app), version, stored))
348 logging.info("{0} has the same version {1} on the Play Store".format(
349 common.getappname(app), version))
355 if options.autoonly and app['Auto Update Mode'] in ('None', 'Static'):
356 logging.debug("Nothing to do for {0}...".format(app['id']))
359 logging.info("Processing " + app['id'] + '...')
361 # If a change is made, commitmsg should be set to a description of it.
362 # Only if this is set will changes be written back to the metadata.
369 mode = app['Update Check Mode']
370 if mode.startswith('Tags'):
371 pattern = mode[5:] if len(mode) > 4 else None
372 (version, vercode, tag) = check_tags(app, pattern)
374 elif mode == 'RepoManifest':
375 (version, vercode) = check_repomanifest(app)
377 elif mode.startswith('RepoManifest/'):
379 (version, vercode) = check_repomanifest(app, tag)
381 elif mode == 'RepoTrunk':
382 (version, vercode) = check_repotrunk(app)
385 (version, vercode) = check_http(app)
387 elif mode in ('None', 'Static'):
389 msg = 'Checking disabled'
393 msg = 'Invalid update check method'
395 if vercode and app['Vercode Operation']:
396 op = app['Vercode Operation'].replace("%c", str(int(vercode)))
397 vercode = str(eval(op))
401 logmsg = "...{0} : {1}".format(app['id'], msg)
406 elif vercode == app['Current Version Code']:
407 logging.info("...up to date")
409 app['Current Version'] = version
410 app['Current Version Code'] = str(int(vercode))
413 # Do the Auto Name thing as well as finding the CV real name
414 if len(app["Repo Type"]) > 0 and mode not in ('None', 'Static'):
418 if app['Repo Type'] == 'srclib':
419 app_dir = os.path.join('build', 'srclib', app['Repo'])
421 app_dir = os.path.join('build/', app['id'])
423 vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
424 vcs.gotorevision(tag)
427 if len(app['builds']) > 0:
428 if 'subdir' in app['builds'][-1]:
429 app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
430 if 'gradle' in app['builds'][-1]:
431 flavour = app['builds'][-1]['gradle']
435 logging.debug("...fetch auto name from " + app_dir +
436 ((" (flavour: %s)" % flavour) if flavour else ""))
437 new_name = common.fetch_real_name(app_dir, flavour)
439 logging.debug("...got autoname '" + new_name + "'")
440 if new_name != app['Auto Name']:
441 app['Auto Name'] = new_name
443 commitmsg = "Set autoname of {0}".format(common.getappname(app))
445 logging.debug("...couldn't get autoname")
447 if app['Current Version'].startswith('@string/'):
448 cv = common.version_name(app['Current Version'], app_dir, flavour)
449 if app['Current Version'] != cv:
450 app['Current Version'] = cv
452 commitmsg = "Fix CV of {0}".format(common.getappname(app))
454 logging.error("Auto Name or Current Version failed for {0} due to exception: {1}".format(app['id'], traceback.format_exc()))
457 name = common.getappname(app)
458 ver = common.getcvname(app)
459 logging.info('...updating to version %s' % ver)
460 commitmsg = 'Update CV of %s to %s' % (name, ver)
463 mode = app['Auto Update Mode']
464 if mode in ('None', 'Static'):
466 elif mode.startswith('Version '):
468 if pattern.startswith('+'):
470 suffix, pattern = pattern.split(' ', 1)
472 raise MetaDataException("Invalid AUM: " + mode)
477 for build in app['builds']:
478 if build['vercode'] == app['Current Version Code']:
480 if not latest or int(build['vercode']) > int(latest['vercode']):
484 newbuild = latest.copy()
485 for k in ('origlines', 'disable'):
488 newbuild['vercode'] = app['Current Version Code']
489 newbuild['version'] = app['Current Version'] + suffix
490 logging.info("...auto-generating build for " + newbuild['version'])
491 commit = pattern.replace('%v', newbuild['version'])
492 commit = commit.replace('%c', newbuild['vercode'])
493 newbuild['commit'] = commit
494 app['builds'].append(newbuild)
495 name = common.getappname(app)
496 ver = common.getcvname(app)
497 commitmsg = "Update %s to %s" % (name, ver)
499 logging.warn('Invalid auto update mode "' + mode + '" on ' + app['id'])
502 metafile = os.path.join('metadata', app['id'] + '.txt')
503 metadata.write_metadata(metafile, app)
505 logging.info("Commiting update for " + metafile)
506 gitcmd = ["git", "commit", "-m",
508 if 'auto_author' in config:
509 gitcmd.extend(['--author', config['auto_author']])
510 gitcmd.extend(["--", metafile])
511 if subprocess.call(gitcmd) != 0:
512 logging.error("Git commit failed")
515 logging.info("Finished.")
517 if __name__ == "__main__":