chiark / gitweb /
fix PEP8 "W391 blank line at end of file"
[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             sys.exit(1)
80         repo = repo[:index]
81         index = repo.find(' ')
82         if index == -1:
83             return (None, "Error while getting repo address - no space? '" + repo + "'")
84         repo = repo[:index]
85         repo = repo.split('"')[0]
86         return (repotype, repo)
87
88     return (None, "No information found." + page)
89
90 config = None
91 options = None
92
93
94 def main():
95
96     global config, options
97
98     # Parse command line...
99     parser = OptionParser()
100     parser.add_option("-v", "--verbose", action="store_true", default=False,
101                       help="Spew out even more information than normal")
102     parser.add_option("-q", "--quiet", action="store_true", default=False,
103                       help="Restrict output to warnings and errors")
104     parser.add_option("-u", "--url", default=None,
105                       help="Project URL to import from.")
106     parser.add_option("-s", "--subdir", default=None,
107                       help="Path to main android project subdirectory, if not in root.")
108     parser.add_option("-r", "--repo", default=None,
109                       help="Allows a different repo to be specified for a multi-repo google code project")
110     parser.add_option("--rev", default=None,
111                       help="Allows a different revision (or git branch) to be specified for the initial import")
112     (options, args) = parser.parse_args()
113
114     config = common.read_config(options)
115
116     if not options.url:
117         logging.info("Specify project url.")
118         sys.exit(1)
119     url = options.url
120
121     tmp_dir = 'tmp'
122     if not os.path.isdir(tmp_dir):
123         logging.info("Creating temporary directory")
124         os.makedirs(tmp_dir)
125
126     # Get all apps...
127     apps = metadata.read_metadata()
128
129     # Figure out what kind of project it is...
130     projecttype = None
131     issuetracker = None
132     license = None
133     website = url #by default, we might override it
134     if url.startswith('git://'):
135         projecttype = 'git'
136         repo = url
137         repotype = 'git'
138         sourcecode = ""
139         website = ""
140     elif url.startswith('https://github.com'):
141         if url.endswith('/'):
142             url = url[:-1]
143         if url.endswith('.git'):
144             logging.info("A github URL should point to the project, not the git repo")
145             sys.exit(1)
146         projecttype = 'github'
147         repo = url + '.git'
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.info("Unable to determine vcs type. " + repo)
166             sys.exit(1)
167     elif url.startswith('http://code.google.com/p/'):
168         if not url.endswith('/'):
169             url += '/'
170         projecttype = 'googlecode'
171         sourcecode = url + 'source/checkout'
172         if options.repo:
173             sourcecode += "?repo=" + options.repo
174         issuetracker = url + 'issues/list'
175
176         # Figure out the repo type and adddress...
177         repotype, repo = getrepofrompage(sourcecode)
178         if not repotype:
179             logging.info("Unable to determine vcs type. " + repo)
180             sys.exit(1)
181
182         # Figure out the license...
183         req = urllib.urlopen(url)
184         if req.getcode() != 200:
185             logging.info('Unable to find project page at ' + sourcecode + ' - return code ' + str(req.getcode()))
186             sys.exit(1)
187         page = req.read()
188         index = page.find('Code license')
189         if index == -1:
190             logging.info("Couldn't find license data")
191             sys.exit(1)
192         ltext = page[index:]
193         lprefix = 'rel="nofollow">'
194         index = ltext.find(lprefix)
195         if index == -1:
196             logging.info("Couldn't find license text")
197             sys.exit(1)
198         ltext = ltext[index + len(lprefix):]
199         index = ltext.find('<')
200         if index == -1:
201             logging.info("License text not formatted as expected")
202             sys.exit(1)
203         ltext = ltext[:index]
204         if ltext == 'GNU GPL v3':
205             license = 'GPLv3'
206         elif ltext == 'GNU GPL v2':
207             license = 'GPLv2'
208         elif ltext == 'Apache License 2.0':
209             license = 'Apache2'
210         elif ltext == 'MIT License':
211             license = 'MIT'
212         elif ltext == 'GNU Lesser GPL':
213             license = 'LGPL'
214         elif ltext == 'Mozilla Public License 1.1':
215             license = 'MPL'
216         elif ltext == 'New BSD License':
217             license = 'NewBSD'
218         else:
219             logging.info("License " + ltext + " is not recognised")
220             sys.exit(1)
221
222     if not projecttype:
223         logging.info("Unable to determine the project type.")
224         logging.info("The URL you supplied was not in one of the supported formats. Please consult")
225         logging.info("the manual for a list of supported formats, and supply one of those.")
226         sys.exit(1)
227
228     # Get a copy of the source so we can extract some info...
229     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
230     src_dir = os.path.join(tmp_dir, 'importer')
231     if os.path.exists(src_dir):
232         shutil.rmtree(src_dir)
233     vcs = common.getvcs(repotype, repo, src_dir)
234     vcs.gotorevision(options.rev)
235     if options.subdir:
236         root_dir = os.path.join(src_dir, options.subdir)
237     else:
238         root_dir = src_dir
239
240     # Extract some information...
241     paths = common.manifest_paths(root_dir, None)
242     if paths:
243
244         version, vercode, package = common.parse_androidmanifests(paths)
245         if not package:
246             logging.info("Couldn't find package ID")
247             sys.exit(1)
248         if not version:
249             logging.warn("Couldn't find latest version name")
250         if not vercode:
251             logging.warn("Couldn't find latest version code")
252     else:
253         spec = os.path.join(root_dir, 'buildozer.spec')
254         if os.path.exists(spec):
255             defaults = {'orientation': 'landscape', 'icon': '',
256                     'permissions': '', 'android.api': "18"}
257             bconfig = ConfigParser(defaults, allow_no_value=True)
258             bconfig.read(spec)
259             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
260             version = bconfig.get('app', 'version')
261             vercode = None
262         else:
263             logging.info("No android or kivy project could be found. Specify --subdir?")
264             sys.exit(1)
265
266     # Make sure it's actually new...
267     for app in apps:
268         if app['id'] == package:
269             logging.info("Package " + package + " already exists")
270             sys.exit(1)
271
272     # Construct the metadata...
273     app = metadata.parse_metadata(None)
274     app['id'] = package
275     app['Web Site'] = website
276     app['Source Code'] = sourcecode
277     if issuetracker:
278         app['Issue Tracker'] = issuetracker
279     if license:
280         app['License'] = license
281     app['Repo Type'] = repotype
282     app['Repo'] = repo
283     app['Update Check Mode'] = "Tags"
284
285     # Create a build line...
286     build = {}
287     build['version'] = version if version else '?'
288     build['vercode'] = vercode if vercode else '?'
289     build['commit'] = '?'
290     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
291     if options.subdir:
292         build['subdir'] = options.subdir
293     if os.path.exists(os.path.join(root_dir, 'jni')):
294         build['buildjni'] = ['yes']
295     app['builds'].append(build)
296
297     # Keep the repo directory to save bandwidth...
298     if not os.path.exists('build'):
299         os.mkdir('build')
300     shutil.move(src_dir, os.path.join('build', package))
301     with open('build/.fdroidvcs-' + package, 'w') as f:
302         f.write(repotype + ' ' + repo)
303
304     metafile = os.path.join('metadata', package + '.txt')
305     metadata.write_metadata(metafile, app)
306     logging.info("Wrote " + metafile)
307
308
309 if __name__ == "__main__":
310     main()