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