xref: /OK3568_Linux_fs/buildroot/utils/scanpypi (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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