]> Git Repo - buildroot-mgba.git/blame - utils/scanpypi
e2fsprogs: fix build for big-endian targets
[buildroot-mgba.git] / utils / scanpypi
CommitLineData
ed0ce495 1#!/usr/bin/env python2
1008ac44
DT
2"""
3
4Utility for building Buildroot packages for existing PyPI packages
5
6Any package built by scanpypi should be manually checked for
7errors.
8"""
9from __future__ import print_function
10import argparse
11import json
12import urllib2
13import sys
14import os
15import shutil
16import StringIO
17import tarfile
18import zipfile
19import errno
20import hashlib
21import re
22import textwrap
23import tempfile
24import imp
25from functools import wraps
26
d2ac1ec6
YY
27try:
28 import spdx_lookup as liclookup
29except 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
36def 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
56import setuptools
57setuptools.setup = setup_decorator(setuptools.setup, 'setuptools')
58import distutils
59distutils.core.setup = setup_decorator(setuptools.setup, 'distutils')
60
61def 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
77def 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
92class DownloadFailed(Exception):
93 pass
94
95class 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
567def 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
676if __name__ == "__main__":
677 main()
This page took 0.299544 seconds and 4 git commands to generate.