chiark / gitweb /
Improve checkupdates error message
[fdroidserver.git] / fdroidserver / checkupdates.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # checkupdates.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013 Daniel Martí <mvdan@mvdan.cc>
7 #
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.
12 #
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.
17 #
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/>.
20
21 import sys
22 import os
23 import re
24 import urllib2
25 import time
26 import subprocess
27 from optparse import OptionParser
28 import traceback
29 import HTMLParser
30 from distutils.version import LooseVersion
31 import common, metadata
32 from common import BuildException
33 from common import VCSException
34
35
36 # Check for a new version by looking at a document retrieved via HTTP.
37 # The app's Update Check Data field is used to provide the information
38 # required.
39 def check_http(app):
40
41     try:
42
43         if not 'Update Check Data' in app:
44             raise Exception('Missing Update Check Data')
45
46         urlcode, codeex, urlver, verex = app['Update Check Data'].split('|')
47
48         vercode = "99999999"
49         if len(urlcode) > 0:
50             print "...requesting {0}".format(urlcode)
51             req = urllib2.Request(urlcode, None)
52             resp = urllib2.urlopen(req, None, 20)
53             page = resp.read()
54
55             m = re.search(codeex, page)
56             if not m:
57                 raise Exception("No RE match for version code")
58             vercode = m.group(1)
59
60         version = "??"
61         if len(urlver) > 0:
62             if urlver != '.':
63                 print "...requesting {0}".format(urlver)
64                 req = urllib2.Request(urlver, None)
65                 resp = urllib2.urlopen(req, None, 20)
66                 page = resp.read()
67
68             m = re.search(verex, page)
69             if not m:
70                 raise Exception("No RE match for version")
71             version = m.group(1)
72
73         return (version, vercode)
74
75     except Exception:
76         msg = "Could not complete http check for app %s due to unknown error: %s" % (app['id'], traceback.format_exc())
77         return (None, msg)
78
79 # Check for a new version by looking at the tags in the source repo.
80 # Whether this can be used reliably or not depends on
81 # the development procedures used by the project's developers. Use it with
82 # caution, because it's inappropriate for many projects.
83 # Returns (None, "a message") if this didn't work, or (version, vercode) for
84 # the details of the current version.
85 def check_tags(app):
86
87     try:
88
89         if app['Repo Type'] == 'srclib':
90             build_dir = os.path.join('build', 'srclib', app['Repo'])
91             repotype = common.getsrclibvcs(app['Repo'])
92         else:
93             build_dir = os.path.join('build/', app['id'])
94             repotype = app['Repo Type']
95
96         if repotype not in ('git', 'git-svn', 'hg', 'bzr'):
97             return (None, 'Tags update mode only works for git, hg, bzr and git-svn repositories currently', None)
98
99         # Set up vcs interface and make sure we have the latest code...
100         vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
101
102         vcs.gotorevision(None)
103
104         flavour = None
105         if len(app['builds']) > 0:
106             if 'subdir' in app['builds'][-1]:
107                 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
108             if 'gradle' in app['builds'][-1]:
109                 flavour = app['builds'][-1]['gradle']
110
111         htag = None
112         hver = None
113         hcode = "0"
114
115         for tag in vcs.gettags():
116             vcs.gotorevision(tag)
117
118             # Only process tags where the manifest exists...
119             paths = common.manifest_paths(build_dir, flavour)
120             version, vercode, package = common.parse_androidmanifests(paths)
121             if package and package == app['id'] and version and vercode:
122                 print "Manifest exists. Found version %s (%s)" % (
123                         version, vercode)
124                 if int(vercode) > int(hcode):
125                     htag = tag
126                     hcode = str(int(vercode))
127                     hver = version
128
129         if hver:
130             return (hver, hcode, htag)
131         return (None, "Couldn't find any version information", None)
132
133     except BuildException as be:
134         msg = "Could not scan app %s due to BuildException: %s" % (app['id'], be)
135         return (None, msg, None)
136     except VCSException as vcse:
137         msg = "VCS error while scanning app %s: %s" % (app['id'], vcse)
138         return (None, msg, None)
139     except Exception:
140         msg = "Could not scan app %s due to unknown error: %s" % (app['id'], traceback.format_exc())
141         return (None, msg, None)
142
143 # Check for a new version by looking at the AndroidManifest.xml at the HEAD
144 # of the source repo. Whether this can be used reliably or not depends on
145 # the development procedures used by the project's developers. Use it with
146 # caution, because it's inappropriate for many projects.
147 # Returns (None, "a message") if this didn't work, or (version, vercode) for
148 # the details of the current version.
149 def check_repomanifest(app, branch=None):
150
151     try:
152
153         if app['Repo Type'] == 'srclib':
154             build_dir = os.path.join('build', 'srclib', app['Repo'])
155             repotype = common.getsrclibvcs(app['Repo'])
156         else:
157             build_dir = os.path.join('build/', app['id'])
158             repotype = app['Repo Type']
159
160         # Set up vcs interface and make sure we have the latest code...
161         vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
162
163         if repotype == 'git':
164             if branch:
165                 branch = 'origin/'+branch
166             vcs.gotorevision(branch)
167         elif repotype == 'git-svn':
168             vcs.gotorevision(branch)
169         elif repotype == 'svn':
170             vcs.gotorevision(None)
171         elif repotype == 'hg':
172             vcs.gotorevision(branch)
173         elif repotype == 'bzr':
174             vcs.gotorevision(None)
175
176         flavour = None
177
178         if len(app['builds']) > 0:
179             if 'subdir' in app['builds'][-1]:
180                 build_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
181             if 'gradle' in app['builds'][-1]:
182                 flavour = app['builds'][-1]['gradle']
183
184         if not os.path.isdir(build_dir):
185             return (None, "Subdir '" + app['builds'][-1]['subdir'] + "'is not a valid directory")
186
187         paths = common.manifest_paths(build_dir, flavour)
188
189         version, vercode, package = common.parse_androidmanifests(paths)
190         if not package:
191             return (None, "Couldn't find package ID")
192         if package != app['id']:
193             return (None, "Package ID mismatch")
194         if not version:
195             return (None,"Couldn't find latest version name")
196         if not vercode:
197             return (None,"Couldn't find latest version code")
198
199         return (version, str(int(vercode)))
200
201     except BuildException as be:
202         msg = "Could not scan app %s due to BuildException: %s" % (app['id'], be)
203         return (None, msg)
204     except VCSException as vcse:
205         msg = "VCS error while scanning app %s: %s" % (app['id'], vcse)
206         return (None, msg)
207     except Exception:
208         msg = "Could not scan app %s due to unknown error: %s" % (app['id'], traceback.format_exc())
209         return (None, msg)
210
211 def check_repotrunk(app, branch=None):
212
213     try:
214         if app['Repo Type'] == 'srclib':
215             build_dir = os.path.join('build', 'srclib', app['Repo'])
216             repotype = common.getsrclibvcs(app['Repo'])
217         else:
218             build_dir = os.path.join('build/', app['id'])
219             repotype = app['Repo Type']
220
221         if repotype not in ('svn', 'git-svn'):
222             return (None, 'RepoTrunk update mode only makes sense in svn and git-svn repositories')
223
224         # Set up vcs interface and make sure we have the latest code...
225         vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
226
227         vcs.gotorevision(None)
228
229         ref = vcs.getref()
230         return (ref, ref)
231     except BuildException as be:
232         msg = "Could not scan app %s due to BuildException: %s" % (app['id'], be)
233         return (None, msg)
234     except VCSException as vcse:
235         msg = "VCS error while scanning app %s: %s" % (app['id'], vcse)
236         return (None, msg)
237     except Exception:
238         msg = "Could not scan app %s due to unknown error: %s" % (app['id'], traceback.format_exc())
239         return (None, msg)
240
241 # Check for a new version by looking at the Google Play Store.
242 # Returns (None, "a message") if this didn't work, or (version, None) for
243 # the details of the current version.
244 def check_gplay(app):
245     time.sleep(15)
246     url = 'https://play.google.com/store/apps/details?id=' + app['id']
247     headers = {'User-Agent' : 'Mozilla/5.0 (X11; Linux i686; rv:18.0) Gecko/20100101 Firefox/18.0'}
248     req = urllib2.Request(url, None, headers)
249     try:
250         resp = urllib2.urlopen(req, None, 20)
251         page = resp.read()
252     except urllib2.HTTPError, e:
253         return (None, str(e.code))
254     except Exception, e:
255         return (None, 'Failed:' + str(e))
256
257     version = None
258
259     m = re.search('itemprop="softwareVersion">[ ]*([^<]+)[ ]*</div>', page)
260     if m:
261         html_parser = HTMLParser.HTMLParser()
262         version = html_parser.unescape(m.group(1))
263
264     if version == 'Varies with device':
265         return (None, 'Device-variable version, cannot use this method')
266
267     if not version:
268         return (None, "Couldn't find version")
269     return (version.strip(), None)
270
271
272 config = None
273 options = None
274
275 def main():
276
277     global config, options
278
279     # Parse command line...
280     parser = OptionParser()
281     parser.add_option("-v", "--verbose", action="store_true", default=False,
282                       help="Spew out even more information than normal")
283     parser.add_option("-p", "--package", default=None,
284                       help="Check only the specified package")
285     parser.add_option("--auto", action="store_true", default=False,
286                       help="Process auto-updates")
287     parser.add_option("--autoonly", action="store_true", default=False,
288                       help="Only process apps with auto-updates")
289     parser.add_option("--commit", action="store_true", default=False,
290                       help="Commit changes")
291     parser.add_option("--gplay", action="store_true", default=False,
292                       help="Only print differences with the Play Store")
293     (options, args) = parser.parse_args()
294
295     config = common.read_config(options)
296
297     # Get all apps...
298     apps = metadata.read_metadata(options.verbose)
299
300     # Filter apps according to command-line options
301     if options.package:
302         apps = [app for app in apps if app['id'] == options.package]
303         if len(apps) == 0:
304             print "No such package"
305             sys.exit(1)
306
307     if options.gplay:
308         for app in apps:
309             version, reason = check_gplay(app)
310             if version is None and options.verbose:
311                 if reason == '404':
312                     print "%s is not in the Play Store" % common.getappname(app)
313                 else:
314                     print "%s encountered a problem: %s" % common.getappname(app)
315             if version is not None:
316                 stored = app['Current Version']
317                 if LooseVersion(stored) < LooseVersion(version):
318                     print "%s has version %s on the Play Store, which is bigger than %s" % (
319                             common.getappname(app), version, stored)
320                 elif options.verbose:
321                     print "%s has the same version %s on the Play Store" % (
322                             common.getappname(app), version)
323         return
324
325
326     for app in apps:
327
328
329         if options.autoonly and app['Auto Update Mode'] == 'None':
330             if options.verbose:
331                 print "Nothing to do for %s..." % app['id']
332             continue
333
334         print "Processing " + app['id'] + '...'
335
336         writeit = False
337         logmsg = None
338
339         tag = None
340         msg = None
341         vercode = None
342         mode = app['Update Check Mode']
343         if mode == 'Tags':
344             (version, vercode, tag) = check_tags(app)
345         elif mode == 'RepoManifest':
346             (version, vercode) = check_repomanifest(app)
347         elif mode.startswith('RepoManifest/'):
348             tag = mode[13:]
349             (version, vercode) = check_repomanifest(app, tag)
350         elif mode == 'RepoTrunk':
351             (version, vercode) = check_repotrunk(app)
352         elif mode == 'HTTP':
353             (version, vercode) = check_http(app)
354         elif mode == 'Static':
355             version = None
356             msg = 'Checking disabled'
357         elif mode == 'None':
358             version = None
359             msg = 'Checking disabled'
360         else:
361             version = None
362             msg = 'Invalid update check method'
363
364         if vercode and app['Vercode Operation']:
365             op = app['Vercode Operation'].replace("%c", str(int(vercode)))
366             vercode = str(eval(op))
367
368         updating = False
369         if not version:
370             print "...%s" % msg
371         elif vercode == app['Current Version Code']:
372             print "...up to date"
373         else:
374             app['Current Version'] = version
375             app['Current Version Code'] = str(int(vercode))
376             updating = True
377             writeit = True
378
379         # Do the Auto Name thing as well as finding the CV real name
380         if len(app["Repo Type"]) > 0:
381
382             try:
383
384                 if app['Repo Type'] == 'srclib':
385                     app_dir = os.path.join('build', 'srclib', app['Repo'])
386                 else:
387                     app_dir = os.path.join('build/', app['id'])
388
389                 vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
390                 vcs.gotorevision(tag)
391
392                 flavour = None
393                 if len(app['builds']) > 0:
394                     if 'subdir' in app['builds'][-1]:
395                         app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
396                     if 'gradle' in app['builds'][-1]:
397                         flavour = app['builds'][-1]['gradle']
398
399                 new_name = common.fetch_real_name(app_dir, flavour)
400                 if new_name != app['Auto Name']:
401                     app['Auto Name'] = new_name
402
403                 if app['Current Version'].startswith('@string/'):
404                     cv = common.version_name(app['Current Version'], app_dir, flavour)
405                     if app['Current Version'] != cv:
406                         app['Current Version'] = cv
407                         writeit = True
408             except Exception:
409                 print "ERROR: Auto Name or Current Version failed for %s due to exception: %s" % (app['id'], traceback.format_exc())
410
411         if updating:
412             name = common.getappname(app)
413             ver = common.getcvname(app)
414             print '...updating to version %s' % ver
415             logmsg = 'Update CV of %s to %s' % (name, ver)
416
417         if options.auto:
418             mode = app['Auto Update Mode']
419             if mode == 'None':
420                 pass
421             elif mode.startswith('Version '):
422                 pattern = mode[8:]
423                 if pattern.startswith('+'):
424                     o = pattern.find(' ')
425                     suffix = pattern[1:o]
426                     pattern = pattern[o + 1:]
427                 else:
428                     suffix = ''
429                 gotcur = False
430                 latest = None
431                 for build in app['builds']:
432                     if build['vercode'] == app['Current Version Code']:
433                         gotcur = True
434                     if not latest or int(build['vercode']) > int(latest['vercode']):
435                         latest = build
436                 if not gotcur:
437                     newbuild = latest.copy()
438                     if 'origlines' in newbuild:
439                         del newbuild['origlines']
440                     newbuild['vercode'] = app['Current Version Code']
441                     newbuild['version'] = app['Current Version'] + suffix
442                     print "...auto-generating build for " + newbuild['version']
443                     commit = pattern.replace('%v', newbuild['version'])
444                     commit = commit.replace('%c', newbuild['vercode'])
445                     newbuild['commit'] = commit
446                     app['builds'].append(newbuild)
447                     writeit = True
448                     name = common.getappname(app)
449                     ver = common.getcvname(app)
450                     logmsg = "Update %s to %s" % (name, ver)
451             else:
452                 print 'Invalid auto update mode "' + mode + '"'
453
454         if writeit:
455             metafile = os.path.join('metadata', app['id'] + '.txt')
456             metadata.write_metadata(metafile, app)
457             if options.commit and logmsg:
458                 print "Commiting update for " + metafile
459                 gitcmd = ["git", "commit", "-m",
460                     logmsg]
461                 if 'auto_author' in config:
462                     gitcmd.extend(['--author', config['auto_author']])
463                 gitcmd.extend(["--", metafile])
464                 if subprocess.call(gitcmd) != 0:
465                     print "Git commit failed"
466                     sys.exit(1)
467
468     print "Finished."
469
470 if __name__ == "__main__":
471     main()
472