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