chiark / gitweb /
e6c2fa1650cb2f555710ebba7c1be3c39a18d3e8
[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     # Get a copy of the source so we can extract some info...
230     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
231     src_dir = os.path.join(tmp_dir, 'importer')
232     if os.path.exists(src_dir):
233         shutil.rmtree(src_dir)
234     vcs = common.getvcs(repotype, repo, src_dir)
235     vcs.gotorevision(options.rev)
236     if options.subdir:
237         root_dir = os.path.join(src_dir, options.subdir)
238     else:
239         root_dir = src_dir
240
241     # Extract some information...
242     paths = common.manifest_paths(root_dir, None)
243     if paths:
244
245         version, vercode, package = common.parse_androidmanifests(paths)
246         if not package:
247             logging.error("Couldn't find package ID")
248             sys.exit(1)
249         if not version:
250             logging.warn("Couldn't find latest version name")
251         if not vercode:
252             logging.warn("Couldn't find latest version code")
253     else:
254         spec = os.path.join(root_dir, 'buildozer.spec')
255         if os.path.exists(spec):
256             defaults = {'orientation': 'landscape', 'icon': '',
257                         'permissions': '', 'android.api': "18"}
258             bconfig = ConfigParser(defaults, allow_no_value=True)
259             bconfig.read(spec)
260             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
261             version = bconfig.get('app', 'version')
262             vercode = None
263         else:
264             logging.error("No android or kivy project could be found. Specify --subdir?")
265             sys.exit(1)
266
267     # Make sure it's actually new...
268     for app in apps:
269         if app['id'] == package:
270             logging.error("Package " + package + " already exists")
271             sys.exit(1)
272
273     # Construct the metadata...
274     app = metadata.parse_metadata(None)
275     app['id'] = package
276     app['Web Site'] = website
277     app['Source Code'] = sourcecode
278     if issuetracker:
279         app['Issue Tracker'] = issuetracker
280     if license:
281         app['License'] = license
282     app['Repo Type'] = repotype
283     app['Repo'] = repo
284     app['Update Check Mode'] = "Tags"
285
286     # Create a build line...
287     build = {}
288     build['version'] = version if version else '?'
289     build['vercode'] = vercode if vercode else '?'
290     build['commit'] = '?'
291     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
292     if options.subdir:
293         build['subdir'] = options.subdir
294     if os.path.exists(os.path.join(root_dir, 'jni')):
295         build['buildjni'] = ['yes']
296
297     for flag, value in metadata.flag_defaults.iteritems():
298         if flag in build:
299             continue
300         build[flag] = value
301
302     app['builds'].append(build)
303
304     # Keep the repo directory to save bandwidth...
305     if not os.path.exists('build'):
306         os.mkdir('build')
307     shutil.move(src_dir, os.path.join('build', package))
308     with open('build/.fdroidvcs-' + package, 'w') as f:
309         f.write(repotype + ' ' + repo)
310
311     metafile = os.path.join('metadata', package + '.txt')
312     metadata.write_metadata(metafile, app)
313     logging.info("Wrote " + metafile)
314
315
316 if __name__ == "__main__":
317     main()