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