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