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