chiark / gitweb /
Only print 'Nothing to do' in verbose mode
[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
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 = common.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 (%s) is not in the Play Store" % (app['Auto Name'], app['id'])
313                 else:
314                     print "%s (%s) encountered a problem: %s" % (app['Auto Name'], app['id'], reason)
315             if version is not None:
316                 stored = app['Current Version']
317                 if LooseVersion(stored) < LooseVersion(version):
318                     print "%s (%s) has version %s on the Play Store, which is bigger than %s" % (
319                             app['Auto Name'], app['id'], version, stored)
320                 elif options.verbose:
321                     print "%s (%s) has the same version %s on the Play Store" % (
322                             app['Auto Name'], app['id'], 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             (version, vercode) = check_repomanifest(app, mode[13:])
349         elif mode == 'RepoTrunk':
350             (version, vercode) = check_repotrunk(app)
351         elif mode == 'HTTP':
352             (version, vercode) = check_http(app)
353         elif mode == 'Static':
354             version = None
355             msg = 'Checking disabled'
356         elif mode == 'None':
357             version = None
358             msg = 'Checking disabled'
359         else:
360             version = None
361             msg = 'Invalid update check method'
362
363         if vercode and app['Vercode Operation']:
364             op = app['Vercode Operation'].replace("%c", str(int(vercode)))
365             vercode = str(eval(op))
366
367         updating = False
368         if not version:
369             print "...%s" % msg
370         elif vercode == app['Current Version Code']:
371             print "...up to date"
372         else:
373             app['Current Version'] = version
374             app['Current Version Code'] = str(int(vercode))
375             updating = True
376             writeit = True
377
378         # Do the Auto Name thing as well as finding the CV real name
379         if len(app["Repo Type"]) > 0:
380
381             try:
382
383                 if app['Repo Type'] == 'srclib':
384                     app_dir = os.path.join('build', 'srclib', app['Repo'])
385                 else:
386                     app_dir = os.path.join('build/', app['id'])
387
388                 vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
389                 vcs.gotorevision(tag)
390
391                 flavour = None
392                 if len(app['builds']) > 0:
393                     if 'subdir' in app['builds'][-1]:
394                         app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
395                     if 'gradle' in app['builds'][-1]:
396                         flavour = app['builds'][-1]['gradle']
397
398                 new_name = common.fetch_real_name(app_dir, flavour)
399                 if new_name != app['Auto Name']:
400                     app['Auto Name'] = new_name
401
402                 if app['Current Version'].startswith('@string/'):
403                     cv = common.version_name(app['Current Version'], app_dir, flavour)
404                     if app['Current Version'] != cv:
405                         app['Current Version'] = cv
406                         writeit = True
407             except Exception:
408                 print "ERROR: Auto Name or Current Version failed for %s due to exception: %s" % (app['id'], traceback.format_exc())
409
410         if updating:
411             print '...updating to version %s (%s)' % (app['Current Version'], app['Current Version Code'])
412             name = '%s (%s)' % (app['Auto Name'], app['id']) if app['Auto Name'] else app['id']
413             ver = "%s (%s)" % (app['Current Version'], app['Current Version Code'])
414             logmsg = 'Update CV of %s to %s' % (name, ver)
415
416         if options.auto:
417             mode = app['Auto Update Mode']
418             if mode == 'None':
419                 pass
420             elif mode.startswith('Version '):
421                 pattern = mode[8:]
422                 if pattern.startswith('+'):
423                     o = pattern.find(' ')
424                     suffix = pattern[1:o]
425                     pattern = pattern[o + 1:]
426                 else:
427                     suffix = ''
428                 gotcur = False
429                 latest = None
430                 for build in app['builds']:
431                     if build['vercode'] == app['Current Version Code']:
432                         gotcur = True
433                     if not latest or int(build['vercode']) > int(latest['vercode']):
434                         latest = build
435                 if not gotcur:
436                     newbuild = latest.copy()
437                     if 'origlines' in newbuild:
438                         del newbuild['origlines']
439                     newbuild['vercode'] = app['Current Version Code']
440                     newbuild['version'] = app['Current Version'] + suffix
441                     print "...auto-generating build for " + newbuild['version']
442                     commit = pattern.replace('%v', newbuild['version'])
443                     commit = commit.replace('%c', newbuild['vercode'])
444                     newbuild['commit'] = commit
445                     app['builds'].append(newbuild)
446                     writeit = True
447                     name = "%s (%s)" % (app['Auto Name'], app['id']) if app['Auto Name'] else app['id']
448                     ver = "%s (%s)" % (newbuild['version'], newbuild['vercode'])
449                     logmsg = "Update %s to %s" % (name, ver)
450             else:
451                 print 'Invalid auto update mode'
452
453         if writeit:
454             metafile = os.path.join('metadata', app['id'] + '.txt')
455             common.write_metadata(metafile, app)
456             if options.commit and logmsg:
457                 print "Commiting update for " + metafile
458                 gitcmd = ["git", "commit", "-m",
459                     logmsg]
460                 if 'auto_author' in config:
461                     gitcmd.extend(['--author', config['auto_author']])
462                 gitcmd.extend(["--", metafile])
463                 if subprocess.call(gitcmd) != 0:
464                     print "Git commit failed"
465                     sys.exit(1)
466
467     print "Finished."
468
469 if __name__ == "__main__":
470     main()
471