chiark / gitweb /
Merge branch 'support-xml-json-yaml-for-metadata' into 'master'
[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 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 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     return (None, "No information found." + page)
68
69 config = None
70 options = None
71
72
73 def main():
74
75     global config, options
76
77     # Parse command line...
78     parser = OptionParser()
79     parser.add_option("-v", "--verbose", action="store_true", default=False,
80                       help="Spew out even more information than normal")
81     parser.add_option("-q", "--quiet", action="store_true", default=False,
82                       help="Restrict output to warnings and errors")
83     parser.add_option("-u", "--url", default=None,
84                       help="Project URL to import from.")
85     parser.add_option("-s", "--subdir", default=None,
86                       help="Path to main android project subdirectory, if not in root.")
87     parser.add_option("--rev", default=None,
88                       help="Allows a different revision (or git branch) to be specified for the initial import")
89     (options, args) = parser.parse_args()
90
91     config = common.read_config(options)
92
93     if not options.url:
94         logging.error("Specify project url.")
95         sys.exit(1)
96     url = options.url
97
98     tmp_dir = 'tmp'
99     if not os.path.isdir(tmp_dir):
100         logging.info("Creating temporary directory")
101         os.makedirs(tmp_dir)
102
103     # Get all apps...
104     apps = metadata.read_metadata()
105
106     # Figure out what kind of project it is...
107     projecttype = None
108     issuetracker = None
109     license = None
110     website = url  # by default, we might override it
111     if url.startswith('git://'):
112         projecttype = 'git'
113         repo = url
114         repotype = 'git'
115         sourcecode = ""
116         website = ""
117     elif url.startswith('https://github.com'):
118         projecttype = 'github'
119         repo = url
120         repotype = 'git'
121         sourcecode = url
122         issuetracker = url + '/issues'
123         website = ""
124     elif url.startswith('https://gitlab.com/'):
125         projecttype = 'gitlab'
126         repo = url
127         repotype = 'git'
128         sourcecode = url + '/tree/HEAD'
129         issuetracker = url + '/issues'
130     elif url.startswith('https://bitbucket.org/'):
131         if url.endswith('/'):
132             url = url[:-1]
133         projecttype = 'bitbucket'
134         sourcecode = url + '/src'
135         issuetracker = url + '/issues'
136         # Figure out the repo type and adddress...
137         repotype, repo = getrepofrompage(sourcecode)
138         if not repotype:
139             logging.error("Unable to determine vcs type. " + repo)
140             sys.exit(1)
141     if not projecttype:
142         logging.error("Unable to determine the project type.")
143         logging.error("The URL you supplied was not in one of the supported formats. Please consult")
144         logging.error("the manual for a list of supported formats, and supply one of those.")
145         sys.exit(1)
146
147     # Ensure we have a sensible-looking repo address at this point. If not, we
148     # might have got a page format we weren't expecting. (Note that we
149     # specifically don't want git@...)
150     if ((repotype != 'bzr' and (not repo.startswith('http://') and
151         not repo.startswith('https://') and
152         not repo.startswith('git://'))) or
153             ' ' in repo):
154         logging.error("Repo address '{0}' does not seem to be valid".format(repo))
155         sys.exit(1)
156
157     # Get a copy of the source so we can extract some info...
158     logging.info('Getting source from ' + repotype + ' repo at ' + repo)
159     src_dir = os.path.join(tmp_dir, 'importer')
160     if os.path.exists(src_dir):
161         shutil.rmtree(src_dir)
162     vcs = common.getvcs(repotype, repo, src_dir)
163     vcs.gotorevision(options.rev)
164     if options.subdir:
165         root_dir = os.path.join(src_dir, options.subdir)
166     else:
167         root_dir = src_dir
168
169     # Extract some information...
170     paths = common.manifest_paths(root_dir, [])
171     if paths:
172
173         version, vercode, package = common.parse_androidmanifests(paths)
174         if not package:
175             logging.error("Couldn't find package ID")
176             sys.exit(1)
177         if not version:
178             logging.warn("Couldn't find latest version name")
179         if not vercode:
180             logging.warn("Couldn't find latest version code")
181     else:
182         spec = os.path.join(root_dir, 'buildozer.spec')
183         if os.path.exists(spec):
184             defaults = {'orientation': 'landscape', 'icon': '',
185                         'permissions': '', 'android.api': "18"}
186             bconfig = ConfigParser(defaults, allow_no_value=True)
187             bconfig.read(spec)
188             package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
189             version = bconfig.get('app', 'version')
190             vercode = None
191         else:
192             logging.error("No android or kivy project could be found. Specify --subdir?")
193             sys.exit(1)
194
195     # Make sure it's actually new...
196     if package in apps:
197         logging.error("Package " + package + " already exists")
198         sys.exit(1)
199
200     # Construct the metadata...
201     app = metadata.parse_txt_metadata(None)[1]
202     app['Web Site'] = website
203     app['Source Code'] = sourcecode
204     if issuetracker:
205         app['Issue Tracker'] = issuetracker
206     if license:
207         app['License'] = license
208     app['Repo Type'] = repotype
209     app['Repo'] = repo
210     app['Update Check Mode'] = "Tags"
211
212     # Create a build line...
213     build = {}
214     build['version'] = version or '?'
215     build['vercode'] = vercode or '?'
216     build['commit'] = '?'
217     build['disable'] = 'Generated by import.py - check/set version fields and commit id'
218     if options.subdir:
219         build['subdir'] = options.subdir
220     if os.path.exists(os.path.join(root_dir, 'jni')):
221         build['buildjni'] = ['yes']
222
223     for flag, value in metadata.flag_defaults.iteritems():
224         if flag in build:
225             continue
226         build[flag] = value
227
228     app['builds'].append(build)
229
230     # Keep the repo directory to save bandwidth...
231     if not os.path.exists('build'):
232         os.mkdir('build')
233     shutil.move(src_dir, os.path.join('build', package))
234     with open('build/.fdroidvcs-' + package, 'w') as f:
235         f.write(repotype + ' ' + repo)
236
237     metadatapath = os.path.join('metadata', package + '.txt')
238     metadata.write_metadata(metadatapath, app)
239     logging.info("Wrote " + metadatapath)
240
241
242 if __name__ == "__main__":
243     main()