chiark / gitweb /
Add some remaining help strings
[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(usage="Usage: %prog [options] [APPID [APPID ...]]")
281     parser.add_option("-v", "--verbose", action="store_true", default=False,
282                       help="Spew out even more information than normal")
283     parser.add_option("--auto", action="store_true", default=False,
284                       help="Process auto-updates")
285     parser.add_option("--autoonly", action="store_true", default=False,
286                       help="Only process apps with auto-updates")
287     parser.add_option("--commit", action="store_true", default=False,
288                       help="Commit changes")
289     parser.add_option("--gplay", action="store_true", default=False,
290                       help="Only print differences with the Play Store")
291     (options, args) = parser.parse_args()
292
293     config = common.read_config(options)
294
295     # Get all apps...
296     allapps = metadata.read_metadata(options.verbose)
297
298     apps = common.read_app_args(args, allapps, False)
299
300     if options.gplay:
301         for app in apps:
302             version, reason = check_gplay(app)
303             if version is None and options.verbose:
304                 if reason == '404':
305                     print "%s is not in the Play Store" % common.getappname(app)
306                 else:
307                     print "%s encountered a problem: %s" % common.getappname(app)
308             if version is not None:
309                 stored = app['Current Version']
310                 if LooseVersion(stored) < LooseVersion(version):
311                     print "%s has version %s on the Play Store, which is bigger than %s" % (
312                             common.getappname(app), version, stored)
313                 elif options.verbose:
314                     print "%s has the same version %s on the Play Store" % (
315                             common.getappname(app), version)
316         return
317
318
319     for app in apps:
320
321         if options.autoonly and app['Auto Update Mode'] == 'None':
322             if options.verbose:
323                 print "Nothing to do for %s..." % app['id']
324             continue
325
326         print "Processing " + app['id'] + '...'
327
328         writeit = False
329         logmsg = None
330
331         tag = None
332         msg = None
333         vercode = None
334         mode = app['Update Check Mode']
335         if mode == 'Tags':
336             (version, vercode, tag) = check_tags(app)
337         elif mode == 'RepoManifest':
338             (version, vercode) = check_repomanifest(app)
339         elif mode.startswith('RepoManifest/'):
340             tag = mode[13:]
341             (version, vercode) = check_repomanifest(app, tag)
342         elif mode == 'RepoTrunk':
343             (version, vercode) = check_repotrunk(app)
344         elif mode == 'HTTP':
345             (version, vercode) = check_http(app)
346         elif mode == 'Static':
347             version = None
348             msg = 'Checking disabled'
349         elif mode == 'None':
350             version = None
351             msg = 'Checking disabled'
352         else:
353             version = None
354             msg = 'Invalid update check method'
355
356         if vercode and app['Vercode Operation']:
357             op = app['Vercode Operation'].replace("%c", str(int(vercode)))
358             vercode = str(eval(op))
359
360         updating = False
361         if not version:
362             print "...%s" % msg
363         elif vercode == app['Current Version Code']:
364             print "...up to date"
365         else:
366             app['Current Version'] = version
367             app['Current Version Code'] = str(int(vercode))
368             updating = True
369             writeit = True
370
371         # Do the Auto Name thing as well as finding the CV real name
372         if len(app["Repo Type"]) > 0:
373
374             try:
375
376                 if app['Repo Type'] == 'srclib':
377                     app_dir = os.path.join('build', 'srclib', app['Repo'])
378                 else:
379                     app_dir = os.path.join('build/', app['id'])
380
381                 vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
382                 vcs.gotorevision(tag)
383
384                 flavour = None
385                 if len(app['builds']) > 0:
386                     if 'subdir' in app['builds'][-1]:
387                         app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
388                     if 'gradle' in app['builds'][-1]:
389                         flavour = app['builds'][-1]['gradle']
390
391                 new_name = common.fetch_real_name(app_dir, flavour)
392                 if new_name != app['Auto Name']:
393                     app['Auto Name'] = new_name
394
395                 if app['Current Version'].startswith('@string/'):
396                     cv = common.version_name(app['Current Version'], app_dir, flavour)
397                     if app['Current Version'] != cv:
398                         app['Current Version'] = cv
399                         writeit = True
400             except Exception:
401                 print "ERROR: Auto Name or Current Version failed for %s due to exception: %s" % (app['id'], traceback.format_exc())
402
403         if updating:
404             name = common.getappname(app)
405             ver = common.getcvname(app)
406             print '...updating to version %s' % ver
407             logmsg = 'Update CV of %s to %s' % (name, ver)
408
409         if options.auto:
410             mode = app['Auto Update Mode']
411             if mode == 'None':
412                 pass
413             elif mode.startswith('Version '):
414                 pattern = mode[8:]
415                 if pattern.startswith('+'):
416                     o = pattern.find(' ')
417                     suffix = pattern[1:o]
418                     pattern = pattern[o + 1:]
419                 else:
420                     suffix = ''
421                 gotcur = False
422                 latest = None
423                 for build in app['builds']:
424                     if build['vercode'] == app['Current Version Code']:
425                         gotcur = True
426                     if not latest or int(build['vercode']) > int(latest['vercode']):
427                         latest = build
428                 if not gotcur:
429                     newbuild = latest.copy()
430                     if 'origlines' in newbuild:
431                         del newbuild['origlines']
432                     newbuild['vercode'] = app['Current Version Code']
433                     newbuild['version'] = app['Current Version'] + suffix
434                     print "...auto-generating build for " + newbuild['version']
435                     commit = pattern.replace('%v', newbuild['version'])
436                     commit = commit.replace('%c', newbuild['vercode'])
437                     newbuild['commit'] = commit
438                     app['builds'].append(newbuild)
439                     writeit = True
440                     name = common.getappname(app)
441                     ver = common.getcvname(app)
442                     logmsg = "Update %s to %s" % (name, ver)
443             else:
444                 print 'Invalid auto update mode "' + mode + '"'
445
446         if writeit:
447             metafile = os.path.join('metadata', app['id'] + '.txt')
448             metadata.write_metadata(metafile, app)
449             if options.commit and logmsg:
450                 print "Commiting update for " + metafile
451                 gitcmd = ["git", "commit", "-m",
452                     logmsg]
453                 if 'auto_author' in config:
454                     gitcmd.extend(['--author', config['auto_author']])
455                 gitcmd.extend(["--", metafile])
456                 if subprocess.call(gitcmd) != 0:
457                     print "Git commit failed"
458                     sys.exit(1)
459
460     print "Finished."
461
462 if __name__ == "__main__":
463     main()
464