chiark / gitweb /
Importer: Fix duplicated urls
[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         website = ""
146     elif url.startswith('https://gitlab.com/'):
147         projecttype = 'gitlab'
148         repo = url
149         repotype = 'git'
150         sourcecode = url + '/tree/HEAD'
151         issuetracker = url + '/issues'
152     elif url.startswith('https://gitorious.org/'):
153         projecttype = 'gitorious'
154         repo = 'https://git.gitorious.org/' + url[22:] + '.git'
155         repotype = 'git'
156         sourcecode = url
157     elif url.startswith('https://bitbucket.org/'):
158         if url.endswith('/'):
159             url = url[:-1]
160         projecttype = 'bitbucket'
161         sourcecode = url + '/src'
162         issuetracker = url + '/issues'
163         # Figure out the repo type and adddress...
164         repotype, repo = getrepofrompage(sourcecode)
165         if not repotype:
166             logging.error("Unable to determine vcs type. " + repo)
167             sys.exit(1)
168     elif (url.startswith('http://code.google.com/p/') or
169             url.startswith('https://code.google.com/p/')):
170         if not url.endswith('/'):
171             url += '/'
172         projecttype = 'googlecode'
173         sourcecode = url + 'source/checkout'
174         if options.repo:
175             sourcecode += "?repo=" + options.repo
176         issuetracker = url + 'issues/list'
177
178         # Figure out the repo type and adddress...
179         repotype, repo = getrepofrompage(sourcecode)
180         if not repotype:
181             logging.error("Unable to determine vcs type. " + repo)
182             sys.exit(1)
183
184         # Figure out the license...
185         req = urllib.urlopen(url)
186         if req.getcode() != 200:
187             logging.error('Unable to find project page at ' + sourcecode + ' - return code ' + str(req.getcode()))
188             sys.exit(1)
189         page = req.read()
190         index = page.find('Code license')
191         if index == -1:
192             logging.error("Couldn't find license data")
193             sys.exit(1)
194         ltext = page[index:]
195         lprefix = 'rel="nofollow">'
196         index = ltext.find(lprefix)
197         if index == -1:
198             logging.error("Couldn't find license text")
199             sys.exit(1)
200         ltext = ltext[index + len(lprefix):]
201         index = ltext.find('<')
202         if index == -1:
203             logging.error("License text not formatted as expected")
204             sys.exit(1)
205         ltext = ltext[:index]
206         if ltext == 'GNU GPL v3':
207             license = 'GPLv3'
208         elif ltext == 'GNU GPL v2':
209             license = 'GPLv2'
210         elif ltext == 'Apache License 2.0':
211             license = 'Apache2'
212         elif ltext == 'MIT License':
213             license = 'MIT'
214         elif ltext == 'GNU Lesser GPL':
215             license = 'LGPL'
216         elif ltext == 'Mozilla Public License 1.1':
217             license = 'MPL'
218         elif ltext == 'New BSD License':
219             license = 'NewBSD'
220         else:
221             logging.error("License " + ltext + " is not recognised")
222             sys.exit(1)
223
224     if not projecttype:
225         logging.error("Unable to determine the project type.")
226         logging.error("The URL you supplied was not in one of the supported formats. Please consult")
227         logging.error("the manual for a list of supported formats, and supply one of those.")
228         sys.exit(1)
229
230     # Ensure we have a sensible-looking repo address at this point. If not, we
231     # might have got a page format we weren't expecting. (Note that we
232     # specifically don't want git@...)
233     if ((repotype != 'bzr' and (not repo.startswith('http://') and
234         not repo.startswith('https://') and
235         not repo.startswith('git://'))) or
236             ' ' in repo):
237         logging.error("Repo address '{0}' does not seem to be valid".format(repo))
238         sys.exit(1)
239
240     # Get a copy of the source so we can extract some info...
241     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
242     src_dir = os.path.join(tmp_dir, 'importer')
243     if os.path.exists(src_dir):
244         shutil.rmtree(src_dir)
245     vcs = common.getvcs(repotype, repo, src_dir)
246     vcs.gotorevision(options.rev)
247     if options.subdir:
248         root_dir = os.path.join(src_dir, options.subdir)
249     else:
250         root_dir = src_dir
251
252     # Extract some information...
253     paths = common.manifest_paths(root_dir, [])
254     if paths:
255
256         version, vercode, package = common.parse_androidmanifests(paths)
257         if not package:
258             logging.error("Couldn't find package ID")
259             sys.exit(1)
260         if not version:
261             logging.warn("Couldn't find latest version name")
262         if not vercode:
263             logging.warn("Couldn't find latest version code")
264     else:
265         spec = os.path.join(root_dir, 'buildozer.spec')
266         if os.path.exists(spec):
267             defaults = {'orientation': 'landscape', 'icon': '',
268                         'permissions': '', 'android.api': "18"}
269             bconfig = ConfigParser(defaults, allow_no_value=True)
270             bconfig.read(spec)
271             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
272             version = bconfig.get('app', 'version')
273             vercode = None
274         else:
275             logging.error("No android or kivy project could be found. Specify --subdir?")
276             sys.exit(1)
277
278     # Make sure it's actually new...
279     if package in apps:
280         logging.error("Package " + package + " already exists")
281         sys.exit(1)
282
283     # Construct the metadata...
284     app = metadata.parse_metadata(None)[1]
285     app['Web Site'] = website
286     app['Source Code'] = sourcecode
287     if issuetracker:
288         app['Issue Tracker'] = issuetracker
289     if license:
290         app['License'] = license
291     app['Repo Type'] = repotype
292     app['Repo'] = repo
293     app['Update Check Mode'] = "Tags"
294
295     # Create a build line...
296     build = {}
297     build['version'] = version or '?'
298     build['vercode'] = vercode or '?'
299     build['commit'] = '?'
300     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
301     if options.subdir:
302         build['subdir'] = options.subdir
303     if os.path.exists(os.path.join(root_dir, 'jni')):
304         build['buildjni'] = ['yes']
305
306     for flag, value in metadata.flag_defaults.iteritems():
307         if flag in build:
308             continue
309         build[flag] = value
310
311     app['builds'].append(build)
312
313     # Keep the repo directory to save bandwidth...
314     if not os.path.exists('build'):
315         os.mkdir('build')
316     shutil.move(src_dir, os.path.join('build', package))
317     with open('build/.fdroidvcs-' + package, 'w') as f:
318         f.write(repotype + ' ' + repo)
319
320     metafile = os.path.join('metadata', package + '.txt')
321     metadata.write_metadata(metafile, app)
322     logging.info("Wrote " + metafile)
323
324
325 if __name__ == "__main__":
326     main()