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