chiark / gitweb /
Improve validation of fdroid import page parsing
[fdroidserver.git] / fdroidserver / import.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # import.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>
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 shutil
24 import urllib
25 from optparse import OptionParser
26 from ConfigParser import ConfigParser
27 import logging
28 import common
29 import metadata
30
31
32 # Get the repo type and address from the given web page. The page is scanned
33 # in a rather naive manner for 'git clone xxxx', 'hg clone xxxx', etc, and
34 # when one of these is found it's assumed that's the information we want.
35 # Returns repotype, address, or None, reason
36 def getrepofrompage(url):
37
38     req = urllib.urlopen(url)
39     if req.getcode() != 200:
40         return (None, 'Unable to get ' + url + ' - return code ' + str(req.getcode()))
41     page = req.read()
42
43     # Works for Google Code and BitBucket...
44     index = page.find('hg clone')
45     if index != -1:
46         repotype = 'hg'
47         repo = page[index + 9:]
48         index = repo.find('<')
49         if index == -1:
50             return (None, "Error while getting repo address")
51         repo = repo[:index]
52         repo = repo.split('"')[0]
53         return (repotype, repo)
54
55     # Works for Google Code and BitBucket...
56     index = page.find('git clone')
57     if index != -1:
58         repotype = 'git'
59         repo = page[index + 10:]
60         index = repo.find('<')
61         if index == -1:
62             return (None, "Error while getting repo address")
63         repo = repo[:index]
64         repo = repo.split('"')[0]
65         return (repotype, repo)
66
67     # Google Code only...
68     index = page.find('svn checkout')
69     if index != -1:
70         repotype = 'git-svn'
71         repo = page[index + 13:]
72         prefix = '<strong><em>http</em></strong>'
73         if not repo.startswith(prefix):
74             return (None, "Unexpected checkout instructions format")
75         repo = 'http' + repo[len(prefix):]
76         index = repo.find('<')
77         if index == -1:
78             return (None, "Error while getting repo address - no end tag? '" + repo + "'")
79         repo = repo[:index]
80         index = repo.find(' ')
81         if index == -1:
82             return (None, "Error while getting repo address - no space? '" + repo + "'")
83         repo = repo[:index]
84         repo = repo.split('"')[0]
85         return (repotype, repo)
86
87     return (None, "No information found." + page)
88
89 config = None
90 options = None
91
92
93 def main():
94
95     global config, options
96
97     # Parse command line...
98     parser = OptionParser()
99     parser.add_option("-v", "--verbose", action="store_true", default=False,
100                       help="Spew out even more information than normal")
101     parser.add_option("-q", "--quiet", action="store_true", default=False,
102                       help="Restrict output to warnings and errors")
103     parser.add_option("-u", "--url", default=None,
104                       help="Project URL to import from.")
105     parser.add_option("-s", "--subdir", default=None,
106                       help="Path to main android project subdirectory, if not in root.")
107     parser.add_option("-r", "--repo", default=None,
108                       help="Allows a different repo to be specified for a multi-repo google code project")
109     parser.add_option("--rev", default=None,
110                       help="Allows a different revision (or git branch) to be specified for the initial import")
111     (options, args) = parser.parse_args()
112
113     config = common.read_config(options)
114
115     if not options.url:
116         logging.error("Specify project url.")
117         sys.exit(1)
118     url = options.url
119
120     tmp_dir = 'tmp'
121     if not os.path.isdir(tmp_dir):
122         logging.info("Creating temporary directory")
123         os.makedirs(tmp_dir)
124
125     # Get all apps...
126     apps = metadata.read_metadata()
127
128     # Figure out what kind of project it is...
129     projecttype = None
130     issuetracker = None
131     license = None
132     website = url  # by default, we might override it
133     if url.startswith('git://'):
134         projecttype = 'git'
135         repo = url
136         repotype = 'git'
137         sourcecode = ""
138         website = ""
139     elif url.startswith('https://github.com'):
140         projecttype = 'github'
141         repo = url
142         repotype = 'git'
143         sourcecode = url
144         issuetracker = url + '/issues'
145     elif url.startswith('https://gitlab.com/'):
146         projecttype = 'gitlab'
147         repo = url
148         repotype = 'git'
149         sourcecode = url
150         issuetracker = url + '/issues'
151     elif url.startswith('https://gitorious.org/'):
152         projecttype = 'gitorious'
153         repo = 'https://git.gitorious.org/' + url[22:] + '.git'
154         repotype = 'git'
155         sourcecode = url
156     elif url.startswith('https://bitbucket.org/'):
157         if url.endswith('/'):
158             url = url[:-1]
159         projecttype = 'bitbucket'
160         sourcecode = url + '/src'
161         issuetracker = url + '/issues'
162         # Figure out the repo type and adddress...
163         repotype, repo = getrepofrompage(sourcecode)
164         if not repotype:
165             logging.error("Unable to determine vcs type. " + repo)
166             sys.exit(1)
167     elif (url.startswith('http://code.google.com/p/') or
168             url.startswith('https://code.google.com/p/')):
169         if not url.endswith('/'):
170             url += '/'
171         projecttype = 'googlecode'
172         sourcecode = url + 'source/checkout'
173         if options.repo:
174             sourcecode += "?repo=" + options.repo
175         issuetracker = url + 'issues/list'
176
177         # Figure out the repo type and adddress...
178         repotype, repo = getrepofrompage(sourcecode)
179         if not repotype:
180             logging.error("Unable to determine vcs type. " + repo)
181             sys.exit(1)
182
183         # Figure out the license...
184         req = urllib.urlopen(url)
185         if req.getcode() != 200:
186             logging.error('Unable to find project page at ' + sourcecode + ' - return code ' + str(req.getcode()))
187             sys.exit(1)
188         page = req.read()
189         index = page.find('Code license')
190         if index == -1:
191             logging.error("Couldn't find license data")
192             sys.exit(1)
193         ltext = page[index:]
194         lprefix = 'rel="nofollow">'
195         index = ltext.find(lprefix)
196         if index == -1:
197             logging.error("Couldn't find license text")
198             sys.exit(1)
199         ltext = ltext[index + len(lprefix):]
200         index = ltext.find('<')
201         if index == -1:
202             logging.error("License text not formatted as expected")
203             sys.exit(1)
204         ltext = ltext[:index]
205         if ltext == 'GNU GPL v3':
206             license = 'GPLv3'
207         elif ltext == 'GNU GPL v2':
208             license = 'GPLv2'
209         elif ltext == 'Apache License 2.0':
210             license = 'Apache2'
211         elif ltext == 'MIT License':
212             license = 'MIT'
213         elif ltext == 'GNU Lesser GPL':
214             license = 'LGPL'
215         elif ltext == 'Mozilla Public License 1.1':
216             license = 'MPL'
217         elif ltext == 'New BSD License':
218             license = 'NewBSD'
219         else:
220             logging.error("License " + ltext + " is not recognised")
221             sys.exit(1)
222
223     if not projecttype:
224         logging.error("Unable to determine the project type.")
225         logging.error("The URL you supplied was not in one of the supported formats. Please consult")
226         logging.error("the manual for a list of supported formats, and supply one of those.")
227         sys.exit(1)
228
229     # Ensure we have a sensible-looking repo address at this point. If not, we
230     # might have got a page format we weren't expecting. (Note that we
231     # specifically don't want git@...)
232     if ((repotype != 'bzr' and (not repo.startswith('http://') and
233         not repo.startswith('https://') and
234         not repo.startswith('git://'))) or
235             ' ' in repo):
236         logging.error("Repo address '{0}' does not seem to be valid".format(repo))
237         sys.exit(1)
238
239     # Get a copy of the source so we can extract some info...
240     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
241     src_dir = os.path.join(tmp_dir, 'importer')
242     if os.path.exists(src_dir):
243         shutil.rmtree(src_dir)
244     vcs = common.getvcs(repotype, repo, src_dir)
245     vcs.gotorevision(options.rev)
246     if options.subdir:
247         root_dir = os.path.join(src_dir, options.subdir)
248     else:
249         root_dir = src_dir
250
251     # Extract some information...
252     paths = common.manifest_paths(root_dir, [])
253     if paths:
254
255         version, vercode, package = common.parse_androidmanifests(paths)
256         if not package:
257             logging.error("Couldn't find package ID")
258             sys.exit(1)
259         if not version:
260             logging.warn("Couldn't find latest version name")
261         if not vercode:
262             logging.warn("Couldn't find latest version code")
263     else:
264         spec = os.path.join(root_dir, 'buildozer.spec')
265         if os.path.exists(spec):
266             defaults = {'orientation': 'landscape', 'icon': '',
267                         'permissions': '', 'android.api': "18"}
268             bconfig = ConfigParser(defaults, allow_no_value=True)
269             bconfig.read(spec)
270             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
271             version = bconfig.get('app', 'version')
272             vercode = None
273         else:
274             logging.error("No android or kivy project could be found. Specify --subdir?")
275             sys.exit(1)
276
277     # Make sure it's actually new...
278     if package in apps:
279         logging.error("Package " + package + " already exists")
280         sys.exit(1)
281
282     # Construct the metadata...
283     app = metadata.parse_metadata(None)[1]
284     app['Web Site'] = website
285     app['Source Code'] = sourcecode
286     if issuetracker:
287         app['Issue Tracker'] = issuetracker
288     if license:
289         app['License'] = license
290     app['Repo Type'] = repotype
291     app['Repo'] = repo
292     app['Update Check Mode'] = "Tags"
293
294     # Create a build line...
295     build = {}
296     build['version'] = version or '?'
297     build['vercode'] = vercode or '?'
298     build['commit'] = '?'
299     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
300     if options.subdir:
301         build['subdir'] = options.subdir
302     if os.path.exists(os.path.join(root_dir, 'jni')):
303         build['buildjni'] = ['yes']
304
305     for flag, value in metadata.flag_defaults.iteritems():
306         if flag in build:
307             continue
308         build[flag] = value
309
310     app['builds'].append(build)
311
312     # Keep the repo directory to save bandwidth...
313     if not os.path.exists('build'):
314         os.mkdir('build')
315     shutil.move(src_dir, os.path.join('build', package))
316     with open('build/.fdroidvcs-' + package, 'w') as f:
317         f.write(repotype + ' ' + repo)
318
319     metafile = os.path.join('metadata', package + '.txt')
320     metadata.write_metadata(metafile, app)
321     logging.info("Wrote " + metafile)
322
323
324 if __name__ == "__main__":
325     main()