chiark / gitweb /
Map apps in memory from appid to appinfo
[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     if package in apps:
269         logging.error("Package " + package + " already exists")
270         sys.exit(1)
271
272     # Construct the metadata...
273     app = metadata.parse_metadata(None)[1]
274     app['Web Site'] = website
275     app['Source Code'] = sourcecode
276     if issuetracker:
277         app['Issue Tracker'] = issuetracker
278     if license:
279         app['License'] = license
280     app['Repo Type'] = repotype
281     app['Repo'] = repo
282     app['Update Check Mode'] = "Tags"
283
284     # Create a build line...
285     build = {}
286     build['version'] = version or '?'
287     build['vercode'] = vercode or '?'
288     build['commit'] = '?'
289     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
290     if options.subdir:
291         build['subdir'] = options.subdir
292     if os.path.exists(os.path.join(root_dir, 'jni')):
293         build['buildjni'] = ['yes']
294
295     for flag, value in metadata.flag_defaults.iteritems():
296         if flag in build:
297             continue
298         build[flag] = value
299
300     app['builds'].append(build)
301
302     # Keep the repo directory to save bandwidth...
303     if not os.path.exists('build'):
304         os.mkdir('build')
305     shutil.move(src_dir, os.path.join('build', package))
306     with open('build/.fdroidvcs-' + package, 'w') as f:
307         f.write(repotype + ' ' + repo)
308
309     metafile = os.path.join('metadata', package + '.txt')
310     metadata.write_metadata(metafile, app)
311     logging.info("Wrote " + metafile)
312
313
314 if __name__ == "__main__":
315     main()