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