chiark / gitweb /
Merge branch 'buildserver-verbose' into 'master'
[fdroidserver.git] / fdroidserver / import.py
1 #!/usr/bin/env python3
2 #
3 # import.py - part of the FDroid server tools
4 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import binascii
21 import os
22 import re
23 import shutil
24 import urllib.request
25 from argparse import ArgumentParser
26 from configparser import ConfigParser
27 import logging
28
29 from . import common
30 from . import metadata
31 from .exception import FDroidException
32
33
34 # Get the repo type and address from the given web page. The page is scanned
35 # in a rather naive manner for 'git clone xxxx', 'hg clone xxxx', etc, and
36 # when one of these is found it's assumed that's the information we want.
37 # Returns repotype, address, or None, reason
38 def getrepofrompage(url):
39
40     req = urllib.request.urlopen(url)
41     if req.getcode() != 200:
42         return (None, 'Unable to get ' + url + ' - return code ' + str(req.getcode()))
43     page = req.read().decode(req.headers.get_content_charset())
44
45     # Works for BitBucket
46     m = re.search('data-fetch-url="(.*)"', page)
47     if m is not None:
48         repo = m.group(1)
49
50         if repo.endswith('.git'):
51             return ('git', repo)
52
53         return ('hg', repo)
54
55     # Works for BitBucket (obsolete)
56     index = page.find('hg clone')
57     if index != -1:
58         repotype = 'hg'
59         repo = page[index + 9:]
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     # Works for BitBucket (obsolete)
68     index = page.find('git clone')
69     if index != -1:
70         repotype = 'git'
71         repo = page[index + 10:]
72         index = repo.find('<')
73         if index == -1:
74             return (None, "Error while getting repo address")
75         repo = repo[:index]
76         repo = repo.split('"')[0]
77         return (repotype, repo)
78
79     return (None, "No information found." + page)
80
81
82 config = None
83 options = None
84
85
86 def get_metadata_from_url(app, url):
87
88     tmp_dir = 'tmp'
89     if not os.path.isdir(tmp_dir):
90         logging.info("Creating temporary directory")
91         os.makedirs(tmp_dir)
92
93     # Figure out what kind of project it is...
94     projecttype = None
95     app.WebSite = url  # by default, we might override it
96     if url.startswith('git://'):
97         projecttype = 'git'
98         repo = url
99         repotype = 'git'
100         app.SourceCode = ""
101         app.WebSite = ""
102     elif url.startswith('https://github.com'):
103         projecttype = 'github'
104         repo = url
105         repotype = 'git'
106         app.SourceCode = url
107         app.IssueTracker = url + '/issues'
108         app.WebSite = ""
109     elif url.startswith('https://gitlab.com/'):
110         projecttype = 'gitlab'
111         # git can be fussy with gitlab URLs unless they end in .git
112         if url.endswith('.git'):
113             url = url[:-4]
114         repo = url + '.git'
115         repotype = 'git'
116         app.WebSite = url
117         app.SourceCode = url + '/tree/HEAD'
118         app.IssueTracker = url + '/issues'
119     elif url.startswith('https://notabug.org/'):
120         projecttype = 'notabug'
121         if url.endswith('.git'):
122             url = url[:-4]
123         repo = url + '.git'
124         repotype = 'git'
125         app.SourceCode = url
126         app.IssueTracker = url + '/issues'
127         app.WebSite = ""
128     elif url.startswith('https://bitbucket.org/'):
129         if url.endswith('/'):
130             url = url[:-1]
131         projecttype = 'bitbucket'
132         app.SourceCode = url + '/src'
133         app.IssueTracker = url + '/issues'
134         # Figure out the repo type and adddress...
135         repotype, repo = getrepofrompage(app.SourceCode)
136         if not repotype:
137             raise FDroidException("Unable to determine vcs type. " + repo)
138     elif url.startswith('https://') and url.endswith('.git'):
139         projecttype = 'git'
140         repo = url
141         repotype = 'git'
142         app.SourceCode = ""
143         app.WebSite = ""
144     if not projecttype:
145         raise FDroidException("Unable to determine the project type. " +
146                               "The URL you supplied was not in one of the supported formats. " +
147                               "Please consult the manual for a list of supported formats, " +
148                               "and supply one of those.")
149
150     # Ensure we have a sensible-looking repo address at this point. If not, we
151     # might have got a page format we weren't expecting. (Note that we
152     # specifically don't want git@...)
153     if ((repotype != 'bzr' and (not repo.startswith('http://') and
154         not repo.startswith('https://') and
155         not repo.startswith('git://'))) or
156             ' ' in repo):
157         raise FDroidException("Repo address '{0}' does not seem to be valid".format(repo))
158
159     # Get a copy of the source so we can extract some info...
160     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
161     build_dir = os.path.join(tmp_dir, 'importer')
162     if os.path.exists(build_dir):
163         shutil.rmtree(build_dir)
164     vcs = common.getvcs(repotype, repo, build_dir)
165     vcs.gotorevision(options.rev)
166     root_dir = get_subdir(build_dir)
167
168     app.RepoType = repotype
169     app.Repo = repo
170
171     return root_dir, build_dir
172
173
174 config = None
175 options = None
176
177
178 def get_subdir(build_dir):
179     if options.subdir:
180         return os.path.join(build_dir, options.subdir)
181
182     return build_dir
183
184
185 def main():
186
187     global config, options
188
189     # Parse command line...
190     parser = ArgumentParser()
191     common.setup_global_opts(parser)
192     parser.add_argument("-u", "--url", default=None,
193                         help="Project URL to import from.")
194     parser.add_argument("-s", "--subdir", default=None,
195                         help="Path to main android project subdirectory, if not in root.")
196     parser.add_argument("-c", "--categories", default=None,
197                         help="Comma separated list of categories.")
198     parser.add_argument("-l", "--license", default=None,
199                         help="Overall license of the project.")
200     parser.add_argument("--rev", default=None,
201                         help="Allows a different revision (or git branch) to be specified for the initial import")
202     metadata.add_metadata_arguments(parser)
203     options = parser.parse_args()
204     metadata.warnings_action = options.W
205
206     config = common.read_config(options)
207
208     apps = metadata.read_metadata()
209     app = metadata.App()
210     app.UpdateCheckMode = "Tags"
211
212     root_dir = None
213     build_dir = None
214
215     local_metadata_files = common.get_local_metadata_files()
216     if local_metadata_files != []:
217         raise FDroidException("This repo already has local metadata: %s" % local_metadata_files[0])
218
219     if options.url is None and os.path.isdir('.git'):
220         app.AutoName = os.path.basename(os.getcwd())
221         app.RepoType = 'git'
222
223         build = {}
224         root_dir = get_subdir(os.getcwd())
225         if os.path.exists('build.gradle'):
226             build.gradle = ['yes']
227
228         import git
229         repo = git.repo.Repo(root_dir)  # git repo
230         for remote in git.Remote.iter_items(repo):
231             if remote.name == 'origin':
232                 url = repo.remotes.origin.url
233                 if url.startswith('https://git'):  # github, gitlab
234                     app.SourceCode = url.rstrip('.git')
235                 app.Repo = url
236                 break
237         # repo.head.commit.binsha is a bytearray stored in a str
238         build.commit = binascii.hexlify(bytearray(repo.head.commit.binsha))
239         write_local_file = True
240     elif options.url:
241         root_dir, build_dir = get_metadata_from_url(app, options.url)
242         build = metadata.Build()
243         build.commit = '?'
244         build.disable = 'Generated by import.py - check/set version fields and commit id'
245         write_local_file = False
246     else:
247         raise FDroidException("Specify project url.")
248
249     # Extract some information...
250     paths = common.manifest_paths(root_dir, [])
251     if paths:
252
253         versionName, versionCode, package = common.parse_androidmanifests(paths, app)
254         if not package:
255             raise FDroidException("Couldn't find package ID")
256         if not versionName:
257             logging.warn("Couldn't find latest version name")
258         if not versionCode:
259             logging.warn("Couldn't find latest version code")
260     else:
261         spec = os.path.join(root_dir, 'buildozer.spec')
262         if os.path.exists(spec):
263             defaults = {'orientation': 'landscape', 'icon': '',
264                         'permissions': '', 'android.api': "18"}
265             bconfig = ConfigParser(defaults, allow_no_value=True)
266             bconfig.read(spec)
267             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
268             versionName = bconfig.get('app', 'version')
269             versionCode = None
270         else:
271             raise FDroidException("No android or kivy project could be found. Specify --subdir?")
272
273     # Make sure it's actually new...
274     if package in apps:
275         raise FDroidException("Package " + package + " already exists")
276
277     # Create a build line...
278     build.versionName = versionName or '?'
279     build.versionCode = versionCode or '?'
280     if options.subdir:
281         build.subdir = options.subdir
282     if options.license:
283         app.License = options.license
284     if options.categories:
285         app.Categories = options.categories
286     if os.path.exists(os.path.join(root_dir, 'jni')):
287         build.buildjni = ['yes']
288     if os.path.exists(os.path.join(root_dir, 'build.gradle')):
289         build.gradle = ['yes']
290
291     metadata.post_metadata_parse(app)
292
293     app.builds.append(build)
294
295     if write_local_file:
296         metadata.write_metadata('.fdroid.yml', app)
297     else:
298         # Keep the repo directory to save bandwidth...
299         if not os.path.exists('build'):
300             os.mkdir('build')
301         if build_dir is not None:
302             shutil.move(build_dir, os.path.join('build', package))
303         with open('build/.fdroidvcs-' + package, 'w') as f:
304             f.write(app.RepoType + ' ' + app.Repo)
305
306         metadatapath = os.path.join('metadata', package + '.txt')
307         metadata.write_metadata(metadatapath, app)
308         logging.info("Wrote " + metadatapath)
309
310
311 if __name__ == "__main__":
312     main()