]>
Commit | Line | Data |
---|---|---|
ed0ce495 | 1 | #!/usr/bin/env python2 |
1008ac44 DT |
2 | """ |
3 | ||
4 | Utility for building Buildroot packages for existing PyPI packages | |
5 | ||
6 | Any package built by scanpypi should be manually checked for | |
7 | errors. | |
8 | """ | |
9 | from __future__ import print_function | |
10 | import argparse | |
11 | import json | |
12 | import urllib2 | |
13 | import sys | |
14 | import os | |
15 | import shutil | |
16 | import StringIO | |
17 | import tarfile | |
18 | import zipfile | |
19 | import errno | |
20 | import hashlib | |
21 | import re | |
22 | import textwrap | |
23 | import tempfile | |
24 | import imp | |
25 | from functools import wraps | |
26 | ||
d2ac1ec6 YY |
27 | try: |
28 | import spdx_lookup as liclookup | |
29 | except ImportError: | |
30 | # spdx_lookup is not installed | |
31 | print('spdx_lookup module is not installed. This can lead to an ' | |
32 | 'inaccurate licence detection. Please install it via\n' | |
33 | 'pip install spdx_lookup') | |
34 | liclookup = None | |
35 | ||
1008ac44 DT |
36 | def setup_decorator(func, method): |
37 | """ | |
38 | Decorator for distutils.core.setup and setuptools.setup. | |
39 | Puts the arguments with which setup is called as a dict | |
40 | Add key 'method' which should be either 'setuptools' or 'distutils'. | |
41 | ||
42 | Keyword arguments: | |
43 | func -- either setuptools.setup or distutils.core.setup | |
44 | method -- either 'setuptools' or 'distutils' | |
45 | """ | |
46 | ||
47 | @wraps(func) | |
48 | def closure(*args, **kwargs): | |
49 | # Any python packages calls its setup function to be installed. | |
50 | # Argument 'name' of this setup function is the package's name | |
51 | BuildrootPackage.setup_args[kwargs['name']] = kwargs | |
52 | BuildrootPackage.setup_args[kwargs['name']]['method'] = method | |
53 | return closure | |
54 | ||
55 | # monkey patch | |
56 | import setuptools | |
57 | setuptools.setup = setup_decorator(setuptools.setup, 'setuptools') | |
58 | import distutils | |
59 | distutils.core.setup = setup_decorator(setuptools.setup, 'distutils') | |
60 | ||
61 | def find_file_upper_case(filenames, path='./'): | |
62 | """ | |
63 | List generator: | |
64 | Recursively find files that matches one of the specified filenames. | |
65 | Returns a relative path starting with path argument. | |
66 | ||
67 | Keyword arguments: | |
68 | filenames -- List of filenames to be found | |
69 | path -- Path to the directory to search | |
70 | """ | |
71 | for root, dirs, files in os.walk(path): | |
72 | for file in files: | |
73 | if file.upper() in filenames: | |
74 | yield (os.path.join(root, file)) | |
75 | ||
76 | ||
77 | def pkg_buildroot_name(pkg_name): | |
78 | """ | |
79 | Returns the Buildroot package name for the PyPI package pkg_name. | |
80 | Remove all non alphanumeric characters except - | |
81 | Also lowers the name and adds 'python-' suffix | |
82 | ||
83 | Keyword arguments: | |
84 | pkg_name -- String to rename | |
85 | """ | |
86 | name = re.sub('[^\w-]', '', pkg_name.lower()) | |
87 | prefix = 'python-' | |
88 | pattern = re.compile('^(?!' + prefix + ')(.+?)$') | |
89 | name = pattern.sub(r'python-\1', name) | |
90 | return name | |
91 | ||
92 | class DownloadFailed(Exception): | |
93 | pass | |
94 | ||
95 | class BuildrootPackage(): | |
96 | """This class's methods are not meant to be used individually please | |
97 | use them in the correct order: | |
98 | ||
99 | __init__ | |
100 | ||
101 | download_package | |
102 | ||
103 | extract_package | |
104 | ||
105 | load_module | |
106 | ||
107 | get_requirements | |
108 | ||
109 | create_package_mk | |
110 | ||
111 | create_hash_file | |
112 | ||
113 | create_config_in | |
114 | ||
115 | """ | |
116 | setup_args = {} | |
117 | ||
118 | def __init__(self, real_name, pkg_folder): | |
119 | self.real_name = real_name | |
120 | self.buildroot_name = pkg_buildroot_name(self.real_name) | |
121 | self.pkg_dir = os.path.join(pkg_folder, self.buildroot_name) | |
122 | self.mk_name = self.buildroot_name.upper().replace('-', '_') | |
123 | self.as_string = None | |
124 | self.md5_sum = None | |
125 | self.metadata = None | |
126 | self.metadata_name = None | |
127 | self.metadata_url = None | |
128 | self.pkg_req = None | |
129 | self.setup_metadata = None | |
130 | self.tmp_extract = None | |
131 | self.used_url = None | |
132 | self.filename = None | |
133 | self.url = None | |
134 | self.version = None | |
135 | ||
136 | def fetch_package_info(self): | |
137 | """ | |
138 | Fetch a package's metadata from the python package index | |
139 | """ | |
140 | self.metadata_url = 'https://pypi.python.org/pypi/{pkg}/json'.format( | |
141 | pkg=self.real_name) | |
142 | try: | |
143 | pkg_json = urllib2.urlopen(self.metadata_url).read().decode() | |
144 | except urllib2.HTTPError as error: | |
145 | print('ERROR:', error.getcode(), error.msg, file=sys.stderr) | |
146 | print('ERROR: Could not find package {pkg}.\n' | |
147 | 'Check syntax inside the python package index:\n' | |
148 | 'https://pypi.python.org/pypi/ ' | |
149 | .format(pkg=self.real_name)) | |
150 | raise | |
151 | except urllib2.URLError: | |
152 | print('ERROR: Could not find package {pkg}.\n' | |
153 | 'Check syntax inside the python package index:\n' | |
154 | 'https://pypi.python.org/pypi/ ' | |
155 | .format(pkg=self.real_name)) | |
156 | raise | |
157 | self.metadata = json.loads(pkg_json) | |
158 | self.version = self.metadata['info']['version'] | |
159 | self.metadata_name = self.metadata['info']['name'] | |
160 | ||
161 | def download_package(self): | |
162 | """ | |
163 | Download a package using metadata from pypi | |
164 | """ | |
165 | try: | |
166 | self.metadata['urls'][0]['filename'] | |
167 | except IndexError: | |
168 | print( | |
169 | 'Non-conventional package, ', | |
170 | 'please check carefully after creation') | |
171 | self.metadata['urls'] = [{ | |
172 | 'packagetype': 'sdist', | |
173 | 'url': self.metadata['info']['download_url'], | |
174 | 'md5_digest': None}] | |
175 | # In this case, we can't get the name of the downloaded file | |
176 | # from the pypi api, so we need to find it, this should work | |
177 | urlpath = urllib2.urlparse.urlparse( | |
178 | self.metadata['info']['download_url']).path | |
179 | # urlparse().path give something like | |
180 | # /path/to/file-version.tar.gz | |
181 | # We use basename to remove /path/to | |
182 | self.metadata['urls'][0]['filename'] = os.path.basename(urlpath) | |
183 | for download_url in self.metadata['urls']: | |
184 | if 'bdist' in download_url['packagetype']: | |
185 | continue | |
186 | try: | |
187 | print('Downloading package {pkg} from {url}...'.format( | |
188 | pkg=self.real_name, url=download_url['url'])) | |
189 | download = urllib2.urlopen(download_url['url']) | |
190 | except urllib2.HTTPError as http_error: | |
191 | download = http_error | |
192 | else: | |
193 | self.used_url = download_url | |
194 | self.as_string = download.read() | |
195 | if not download_url['md5_digest']: | |
196 | break | |
197 | self.md5_sum = hashlib.md5(self.as_string).hexdigest() | |
198 | if self.md5_sum == download_url['md5_digest']: | |
199 | break | |
200 | else: | |
201 | if download.__class__ == urllib2.HTTPError: | |
202 | raise download | |
203 | raise DownloadFailed('Failed to downloas package {pkg}' | |
204 | .format(pkg=self.real_name)) | |
205 | self.filename = self.used_url['filename'] | |
206 | self.url = self.used_url['url'] | |
207 | ||
208 | def extract_package(self, tmp_path): | |
209 | """ | |
210 | Extract the package contents into a directrory | |
211 | ||
212 | Keyword arguments: | |
213 | tmp_path -- directory where you want the package to be extracted | |
214 | """ | |
215 | as_file = StringIO.StringIO(self.as_string) | |
216 | if self.filename[-3:] == 'zip': | |
217 | with zipfile.ZipFile(as_file) as as_zipfile: | |
218 | tmp_pkg = os.path.join(tmp_path, self.buildroot_name) | |
219 | try: | |
220 | os.makedirs(tmp_pkg) | |
221 | except OSError as exception: | |
222 | if exception.errno != errno.EEXIST: | |
223 | print("ERROR: ", exception.message, file=sys.stderr) | |
224 | return None, None | |
225 | print('WARNING:', exception.message, file=sys.stderr) | |
226 | print('Removing {pkg}...'.format(pkg=tmp_pkg)) | |
227 | shutil.rmtree(tmp_pkg) | |
228 | os.makedirs(tmp_pkg) | |
229 | as_zipfile.extractall(tmp_pkg) | |
230 | else: | |
231 | with tarfile.open(fileobj=as_file) as as_tarfile: | |
232 | tmp_pkg = os.path.join(tmp_path, self.buildroot_name) | |
233 | try: | |
234 | os.makedirs(tmp_pkg) | |
235 | except OSError as exception: | |
236 | if exception.errno != errno.EEXIST: | |
237 | print("ERROR: ", exception.message, file=sys.stderr) | |
238 | return None, None | |
239 | print('WARNING:', exception.message, file=sys.stderr) | |
240 | print('Removing {pkg}...'.format(pkg=tmp_pkg)) | |
241 | shutil.rmtree(tmp_pkg) | |
242 | os.makedirs(tmp_pkg) | |
243 | as_tarfile.extractall(tmp_pkg) | |
244 | ||
245 | tmp_extract = '{folder}/{name}-{version}' | |
246 | self.tmp_extract = tmp_extract.format( | |
247 | folder=tmp_pkg, | |
248 | name=self.metadata_name, | |
249 | version=self.version) | |
250 | ||
251 | def load_setup(self): | |
252 | """ | |
253 | Loads the corresponding setup and store its metadata | |
254 | """ | |
255 | current_dir = os.getcwd() | |
256 | os.chdir(self.tmp_extract) | |
257 | sys.path.append(self.tmp_extract) | |
258 | s_file, s_path, s_desc = imp.find_module('setup', [self.tmp_extract]) | |
259 | setup = imp.load_module('setup', s_file, s_path, s_desc) | |
260 | try: | |
261 | self.setup_metadata = self.setup_args[self.metadata_name] | |
262 | except KeyError: | |
263 | # This means setup was not called which most likely mean that it is | |
264 | # called through the if __name__ == '__main__' directive. | |
265 | # In this case, we can only pray that it is called through a | |
266 | # function called main() in setup.py. | |
c2877a2a | 267 | setup.main() # Will raise AttributeError if not found |
1008ac44 DT |
268 | self.setup_metadata = self.setup_args[self.metadata_name] |
269 | # Here we must remove the module the hard way. | |
9eebeb44 | 270 | # We must do this because of a very specific case: if a package calls |
1008ac44 | 271 | # setup from the __main__ but does not come with a 'main()' function, |
c2877a2a | 272 | # for some reason setup.main() will successfully call the main |
1008ac44 DT |
273 | # function of a previous package... |
274 | sys.modules.pop('setup',None) | |
275 | del setup | |
276 | os.chdir(current_dir) | |
277 | sys.path.remove(self.tmp_extract) | |
278 | ||
279 | def get_requirements(self, pkg_folder): | |
280 | """ | |
281 | Retrieve dependencies from the metadata found in the setup.py script of | |
282 | a pypi package. | |
283 | ||
284 | Keyword Arguments: | |
285 | pkg_folder -- location of the already created packages | |
286 | """ | |
287 | if 'install_requires' not in self.setup_metadata: | |
288 | self.pkg_req = None | |
289 | return set() | |
290 | self.pkg_req = self.setup_metadata['install_requires'] | |
291 | self.pkg_req = [re.sub('([-.\w]+).*', r'\1', req) | |
292 | for req in self.pkg_req] | |
293 | req_not_found = self.pkg_req | |
294 | self.pkg_req = map(pkg_buildroot_name, self.pkg_req) | |
295 | pkg_tuples = zip(req_not_found, self.pkg_req) | |
296 | # pkg_tuples is a list of tuples that looks like | |
297 | # ('werkzeug','python-werkzeug') because I need both when checking if | |
298 | # dependencies already exist or are already in the download list | |
299 | req_not_found = set( | |
300 | pkg[0] for pkg in pkg_tuples | |
301 | if not os.path.isdir(pkg[1]) | |
302 | ) | |
303 | return req_not_found | |
304 | ||
305 | def __create_mk_header(self): | |
306 | """ | |
307 | Create the header of the <package_name>.mk file | |
308 | """ | |
309 | header = ['#' * 80 + '\n'] | |
310 | header.append('#\n') | |
311 | header.append('# {name}\n'.format(name=self.buildroot_name)) | |
312 | header.append('#\n') | |
313 | header.append('#' * 80 + '\n') | |
314 | header.append('\n') | |
315 | return header | |
316 | ||
317 | def __create_mk_download_info(self): | |
318 | """ | |
319 | Create the lines refering to the download information of the | |
320 | <package_name>.mk file | |
321 | """ | |
322 | lines = [] | |
323 | version_line = '{name}_VERSION = {version}\n'.format( | |
324 | name=self.mk_name, | |
325 | version=self.version) | |
326 | lines.append(version_line) | |
327 | ||
328 | targz = self.filename.replace( | |
329 | self.version, | |
330 | '$({name}_VERSION)'.format(name=self.mk_name)) | |
331 | targz_line = '{name}_SOURCE = {filename}\n'.format( | |
332 | name=self.mk_name, | |
333 | filename=targz) | |
334 | lines.append(targz_line) | |
335 | ||
336 | if self.filename not in self.url: | |
337 | # Sometimes the filename is in the url, sometimes it's not | |
338 | site_url = self.url | |
339 | else: | |
340 | site_url = self.url[:self.url.find(self.filename)] | |
341 | site_line = '{name}_SITE = {url}'.format(name=self.mk_name, | |
342 | url=site_url) | |
343 | site_line = site_line.rstrip('/') + '\n' | |
344 | lines.append(site_line) | |
345 | return lines | |
346 | ||
347 | def __create_mk_setup(self): | |
348 | """ | |
349 | Create the line refering to the setup method of the package of the | |
350 | <package_name>.mk file | |
351 | ||
352 | There are two things you can use to make an installer | |
353 | for a python package: distutils or setuptools | |
354 | distutils comes with python but does not support dependencies. | |
355 | distutils is mostly still there for backward support. | |
356 | setuptools is what smart people use, | |
357 | but it is not shipped with python :( | |
358 | """ | |
359 | lines = [] | |
360 | setup_type_line = '{name}_SETUP_TYPE = {method}\n'.format( | |
361 | name=self.mk_name, | |
362 | method=self.setup_metadata['method']) | |
363 | lines.append(setup_type_line) | |
364 | return lines | |
365 | ||
d2ac1ec6 | 366 | def __get_license_names(self, license_files): |
1008ac44 | 367 | """ |
d2ac1ec6 YY |
368 | Try to determine the related license name. |
369 | ||
370 | There are two possibilities. Either the scripts tries to | |
371 | get license name from package's metadata or, if spdx_lookup | |
372 | package is available, the script compares license files with | |
373 | SPDX database. | |
374 | """ | |
375 | license_line = '' | |
376 | if liclookup is None: | |
377 | license_dict = { | |
378 | 'Apache Software License': 'Apache-2.0', | |
379 | 'BSD License': 'BSD', | |
380 | 'European Union Public Licence 1.0': 'EUPL-1.0', | |
381 | 'European Union Public Licence 1.1': 'EUPL-1.1', | |
382 | "GNU General Public License": "GPL", | |
383 | "GNU General Public License v2": "GPL-2.0", | |
384 | "GNU General Public License v2 or later": "GPL-2.0+", | |
385 | "GNU General Public License v3": "GPL-3.0", | |
386 | "GNU General Public License v3 or later": "GPL-3.0+", | |
387 | "GNU Lesser General Public License v2": "LGPL-2.1", | |
388 | "GNU Lesser General Public License v2 or later": "LGPL-2.1+", | |
389 | "GNU Lesser General Public License v3": "LGPL-3.0", | |
390 | "GNU Lesser General Public License v3 or later": "LGPL-3.0+", | |
391 | "GNU Library or Lesser General Public License": "LGPL-2.0", | |
392 | "ISC License": "ISC", | |
393 | "MIT License": "MIT", | |
394 | "Mozilla Public License 1.0": "MPL-1.0", | |
395 | "Mozilla Public License 1.1": "MPL-1.1", | |
396 | "Mozilla Public License 2.0": "MPL-2.0", | |
397 | "Zope Public License": "ZPL" | |
398 | } | |
399 | regexp = re.compile('^License :* *.* *:+ (.*)( \(.*\))?$') | |
400 | classifiers_licenses = [regexp.sub(r"\1", lic) | |
401 | for lic in self.metadata['info']['classifiers'] | |
402 | if regexp.match(lic)] | |
403 | licenses = map(lambda x: license_dict[x] if x in license_dict else x, | |
404 | classifiers_licenses) | |
405 | if not len(licenses): | |
406 | print('WARNING: License has been set to "{license}". It is most' | |
407 | ' likely wrong, please change it if need be'.format( | |
408 | license=', '.join(licenses))) | |
409 | licenses = [self.metadata['info']['license']] | |
410 | license_line = '{name}_LICENSE = {license}\n'.format( | |
411 | name=self.mk_name, | |
412 | license=', '.join(licenses)) | |
413 | else: | |
414 | license_names = [] | |
415 | for license_file in license_files: | |
416 | with open(license_file) as lic_file: | |
417 | match = liclookup.match(lic_file.read()) | |
418 | if match.confidence >= 90.0: | |
419 | license_names.append(match.license.id) | |
1008ac44 | 420 | |
d2ac1ec6 YY |
421 | if len(license_names) > 0: |
422 | license_line = ('{name}_LICENSE =' | |
423 | ' {names}\n'.format( | |
424 | name=self.mk_name, | |
425 | names=', '.join(license_names))) | |
1008ac44 | 426 | |
d2ac1ec6 | 427 | return license_line |
1008ac44 | 428 | |
d2ac1ec6 YY |
429 | def __create_mk_license(self): |
430 | """ | |
431 | Create the lines referring to the package's license informations of the | |
432 | <package_name>.mk file | |
1008ac44 | 433 | |
d2ac1ec6 YY |
434 | The license's files are found by searching the package (case insensitive) |
435 | for files named license, license.txt etc. If more than one license file | |
436 | is found, the user is asked to select which ones he wants to use. | |
1008ac44 | 437 | """ |
1008ac44 | 438 | lines = [] |
1008ac44 | 439 | |
caa37600 | 440 | filenames = ['LICENCE', 'LICENSE', 'LICENSE.RST', 'LICENSE.TXT', |
d2ac1ec6 | 441 | 'COPYING', 'COPYING.TXT'] |
1008ac44 | 442 | license_files = list(find_file_upper_case(filenames, self.tmp_extract)) |
d2ac1ec6 YY |
443 | |
444 | lines.append(self.__get_license_names(license_files)) | |
445 | ||
1008ac44 DT |
446 | license_files = [license.replace(self.tmp_extract, '')[1:] |
447 | for license in license_files] | |
448 | if len(license_files) > 0: | |
449 | if len(license_files) > 1: | |
450 | print('More than one file found for license:', | |
451 | ', '.join(license_files)) | |
452 | license_files = [filename | |
453 | for index, filename in enumerate(license_files)] | |
454 | license_file_line = ('{name}_LICENSE_FILES =' | |
455 | ' {files}\n'.format( | |
456 | name=self.mk_name, | |
457 | files=' '.join(license_files))) | |
458 | lines.append(license_file_line) | |
459 | else: | |
460 | print('WARNING: No license file found,' | |
461 | ' please specify it manually afterwards') | |
462 | license_file_line = '# No license file found\n' | |
463 | ||
464 | return lines | |
465 | ||
466 | def __create_mk_requirements(self): | |
467 | """ | |
468 | Create the lines referring to the dependencies of the of the | |
469 | <package_name>.mk file | |
470 | ||
471 | Keyword Arguments: | |
472 | pkg_name -- name of the package | |
473 | pkg_req -- dependencies of the package | |
474 | """ | |
475 | lines = [] | |
476 | dependencies_line = ('{name}_DEPENDENCIES =' | |
477 | ' {reqs}\n'.format( | |
478 | name=self.mk_name, | |
479 | reqs=' '.join(self.pkg_req))) | |
480 | lines.append(dependencies_line) | |
481 | return lines | |
482 | ||
483 | def create_package_mk(self): | |
484 | """ | |
485 | Create the lines corresponding to the <package_name>.mk file | |
486 | """ | |
487 | pkg_mk = '{name}.mk'.format(name=self.buildroot_name) | |
488 | path_to_mk = os.path.join(self.pkg_dir, pkg_mk) | |
489 | print('Creating {file}...'.format(file=path_to_mk)) | |
490 | lines = self.__create_mk_header() | |
491 | lines += self.__create_mk_download_info() | |
492 | lines += self.__create_mk_setup() | |
493 | lines += self.__create_mk_license() | |
1008ac44 DT |
494 | |
495 | lines.append('\n') | |
496 | lines.append('$(eval $(python-package))') | |
497 | lines.append('\n') | |
498 | with open(path_to_mk, 'w') as mk_file: | |
499 | mk_file.writelines(lines) | |
500 | ||
501 | def create_hash_file(self): | |
502 | """ | |
503 | Create the lines corresponding to the <package_name>.hash files | |
504 | """ | |
505 | pkg_hash = '{name}.hash'.format(name=self.buildroot_name) | |
506 | path_to_hash = os.path.join(self.pkg_dir, pkg_hash) | |
507 | print('Creating {filename}...'.format(filename=path_to_hash)) | |
508 | lines = [] | |
509 | if self.used_url['md5_digest']: | |
2f4bd0f3 YY |
510 | md5_comment = '# md5 from {url}, sha256 locally computed\n'.format( |
511 | url=self.metadata_url) | |
1008ac44 DT |
512 | lines.append(md5_comment) |
513 | hash_line = '{method}\t{digest} {filename}\n'.format( | |
514 | method='md5', | |
515 | digest=self.used_url['md5_digest'], | |
516 | filename=self.filename) | |
517 | lines.append(hash_line) | |
1008ac44 DT |
518 | digest = hashlib.sha256(self.as_string).hexdigest() |
519 | hash_line = '{method}\t{digest} {filename}\n'.format( | |
520 | method='sha256', | |
521 | digest=digest, | |
522 | filename=self.filename) | |
523 | lines.append(hash_line) | |
524 | ||
525 | with open(path_to_hash, 'w') as hash_file: | |
526 | hash_file.writelines(lines) | |
527 | ||
528 | def create_config_in(self): | |
529 | """ | |
530 | Creates the Config.in file of a package | |
531 | """ | |
532 | path_to_config = os.path.join(self.pkg_dir, 'Config.in') | |
533 | print('Creating {file}...'.format(file=path_to_config)) | |
534 | lines = [] | |
535 | config_line = 'config BR2_PACKAGE_{name}\n'.format( | |
536 | name=self.mk_name) | |
537 | lines.append(config_line) | |
538 | ||
539 | bool_line = '\tbool "{name}"\n'.format(name=self.buildroot_name) | |
540 | lines.append(bool_line) | |
541 | if self.pkg_req: | |
542 | for dep in self.pkg_req: | |
8a64ade2 | 543 | dep_line = '\tselect BR2_PACKAGE_{req} # runtime\n'.format( |
1008ac44 DT |
544 | req=dep.upper().replace('-', '_')) |
545 | lines.append(dep_line) | |
546 | ||
547 | lines.append('\thelp\n') | |
548 | ||
549 | help_lines = textwrap.wrap(self.metadata['info']['summary'], | |
550 | initial_indent='\t ', | |
551 | subsequent_indent='\t ') | |
f49295cc YY |
552 | |
553 | # make sure a help text is terminated with a full stop | |
554 | if help_lines[-1][-1] != '.': | |
555 | help_lines[-1] += '.' | |
556 | ||
1008ac44 DT |
557 | # \t + two spaces is 3 char long |
558 | help_lines.append('') | |
559 | help_lines.append('\t ' + self.metadata['info']['home_page']) | |
560 | help_lines = map(lambda x: x + '\n', help_lines) | |
561 | lines += help_lines | |
562 | ||
563 | with open(path_to_config, 'w') as config_file: | |
564 | config_file.writelines(lines) | |
565 | ||
566 | ||
567 | def main(): | |
568 | # Building the parser | |
569 | parser = argparse.ArgumentParser( | |
570 | description="Creates buildroot packages from the metadata of " | |
571 | "an existing PyPI packages and include it " | |
572 | "in menuconfig") | |
573 | parser.add_argument("packages", | |
574 | help="list of packages to be created", | |
575 | nargs='+') | |
576 | parser.add_argument("-o", "--output", | |
577 | help=""" | |
578 | Output directory for packages. | |
579 | Default is ./package | |
580 | """, | |
581 | default='./package') | |
582 | ||
583 | args = parser.parse_args() | |
584 | packages = list(set(args.packages)) | |
585 | ||
586 | # tmp_path is where we'll extract the files later | |
587 | tmp_prefix = 'scanpypi-' | |
588 | pkg_folder = args.output | |
589 | tmp_path = tempfile.mkdtemp(prefix=tmp_prefix) | |
590 | try: | |
591 | for real_pkg_name in packages: | |
592 | package = BuildrootPackage(real_pkg_name, pkg_folder) | |
593 | print('buildroot package name for {}:'.format(package.real_name), | |
594 | package.buildroot_name) | |
595 | # First we download the package | |
596 | # Most of the info we need can only be found inside the package | |
597 | print('Package:', package.buildroot_name) | |
598 | print('Fetching package', package.real_name) | |
599 | try: | |
600 | package.fetch_package_info() | |
601 | except (urllib2.URLError, urllib2.HTTPError): | |
602 | continue | |
603 | if package.metadata_name.lower() == 'setuptools': | |
604 | # setuptools imports itself, that does not work very well | |
605 | # with the monkey path at the begining | |
606 | print('Error: setuptools cannot be built using scanPyPI') | |
607 | continue | |
608 | ||
609 | try: | |
610 | package.download_package() | |
611 | except urllib2.HTTPError as error: | |
612 | print('Error: {code} {reason}'.format(code=error.code, | |
613 | reason=error.reason)) | |
614 | print('Error downloading package :', package.buildroot_name) | |
615 | print() | |
616 | continue | |
617 | ||
618 | # extract the tarball | |
619 | try: | |
620 | package.extract_package(tmp_path) | |
621 | except (tarfile.ReadError, zipfile.BadZipfile): | |
622 | print('Error extracting package {}'.format(package.real_name)) | |
623 | print() | |
624 | continue | |
625 | ||
626 | # Loading the package install info from the package | |
627 | try: | |
628 | package.load_setup() | |
629 | except ImportError as err: | |
630 | if 'buildutils' in err.message: | |
631 | print('This package needs buildutils') | |
632 | else: | |
633 | raise | |
634 | continue | |
5d2c69da AR |
635 | except AttributeError as error: |
636 | print('Error: Could not install package {pkg}: {error}'.format( | |
637 | pkg=package.real_name, error=error)) | |
1008ac44 DT |
638 | continue |
639 | ||
640 | # Package requirement are an argument of the setup function | |
641 | req_not_found = package.get_requirements(pkg_folder) | |
642 | req_not_found = req_not_found.difference(packages) | |
643 | ||
644 | packages += req_not_found | |
645 | if req_not_found: | |
646 | print('Added packages \'{pkgs}\' as dependencies of {pkg}' | |
647 | .format(pkgs=", ".join(req_not_found), | |
648 | pkg=package.buildroot_name)) | |
649 | print('Checking if package {name} already exists...'.format( | |
650 | name=package.pkg_dir)) | |
651 | try: | |
652 | os.makedirs(package.pkg_dir) | |
653 | except OSError as exception: | |
654 | if exception.errno != errno.EEXIST: | |
655 | print("ERROR: ", exception.message, file=sys.stderr) | |
656 | continue | |
657 | print('Error: Package {name} already exists' | |
658 | .format(name=package.pkg_dir)) | |
659 | del_pkg = raw_input( | |
660 | 'Do you want to delete existing package ? [y/N]') | |
661 | if del_pkg.lower() == 'y': | |
662 | shutil.rmtree(package.pkg_dir) | |
663 | os.makedirs(package.pkg_dir) | |
664 | else: | |
665 | continue | |
666 | package.create_package_mk() | |
667 | ||
668 | package.create_hash_file() | |
669 | ||
670 | package.create_config_in() | |
671 | print() | |
672 | # printing an empty line for visual confort | |
673 | finally: | |
674 | shutil.rmtree(tmp_path) | |
675 | ||
676 | if __name__ == "__main__": | |
677 | main() |