Home | History | Annotate | Download | only in py3
      1 # -*- coding: ascii -*-
      2 #
      3 # Copyright 2007 - 2013
      4 # Andr\xe9 Malo or his licensors, as applicable
      5 #
      6 # Licensed under the Apache License, Version 2.0 (the "License");
      7 # you may not use this file except in compliance with the License.
      8 # You may obtain a copy of the License at
      9 #
     10 #     http://www.apache.org/licenses/LICENSE-2.0
     11 #
     12 # Unless required by applicable law or agreed to in writing, software
     13 # distributed under the License is distributed on an "AS IS" BASIS,
     14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15 # See the License for the specific language governing permissions and
     16 # limitations under the License.
     17 """
     18 ===================
     19  Main setup runner
     20 ===================
     21 
     22 This module provides a wrapper around the distutils core setup.
     23 """
     24 __author__ = "Andr\xe9 Malo"
     25 __docformat__ = "restructuredtext en"
     26 
     27 import configparser as _config_parser
     28 from distutils import core as _core
     29 import os as _os
     30 import posixpath as _posixpath
     31 import sys as _sys
     32 
     33 from _setup import commands as _commands
     34 from _setup import data as _data
     35 from _setup import ext as _ext
     36 from _setup import util as _util
     37 from _setup import shell as _shell
     38 
     39 
     40 def check_python_version(impl, version_min, version_max):
     41     """ Check python version """
     42     if impl == 'python':
     43         version_info = _sys.version_info
     44     elif impl == 'pypy':
     45         version_info = getattr(_sys, 'pypy_version_info', None)
     46         if not version_info:
     47             return
     48     elif impl == 'jython':
     49         if not 'java' in _sys.platform.lower():
     50             return
     51         version_info = _sys.version_info
     52     else:
     53         raise AssertionError("impl not in ('python', 'pypy', 'jython')")
     54 
     55     pyversion = list(map(int, version_info[:3]))
     56     if version_min:
     57         min_required = list(
     58             map(int, '.'.join((version_min, '0.0.0')).split('.')[:3])
     59         )
     60         if pyversion < min_required:
     61             raise EnvironmentError("Need at least %s %s (vs. %s)" % (
     62                 impl, version_min, '.'.join(map(str, pyversion))
     63             ))
     64     if version_max:
     65         max_required = list(map(int, version_max.split('.')))
     66         max_required[-1] += 1
     67         if pyversion >= max_required:
     68             raise EnvironmentError("Need at max %s %s (vs. %s)" % (
     69                 impl,
     70                 version_max,
     71                 '.'.join(map(str, pyversion))
     72             ))
     73 
     74 
     75 def find_description(docs):
     76     """
     77     Determine the package description from DESCRIPTION
     78 
     79     :Parameters:
     80       `docs` : ``dict``
     81         Docs config section
     82 
     83     :Return: Tuple of summary, description and license
     84              (``('summary', 'description', 'license')``)
     85              (all may be ``None``)
     86     :Rtype: ``tuple``
     87     """
     88     summary = None
     89     filename = docs.get('meta.summary', 'SUMMARY').strip()
     90     if filename and _os.path.isfile(filename):
     91         fp = open(filename, encoding='utf-8')
     92         try:
     93             try:
     94                 summary = fp.read().strip().splitlines()[0].rstrip()
     95             except IndexError:
     96                 summary = ''
     97         finally:
     98             fp.close()
     99 
    100     description = None
    101     filename = docs.get('meta.description', 'DESCRIPTION').strip()
    102     if filename and _os.path.isfile(filename):
    103         fp = open(filename, encoding='utf-8')
    104         try:
    105             description = fp.read().rstrip()
    106         finally:
    107             fp.close()
    108 
    109         if summary is None and description:
    110             from docutils import core
    111             summary = core.publish_parts(
    112                 source=description,
    113                 source_path=filename,
    114                 writer_name='html',
    115             )['title'].encode('utf-8')
    116 
    117     return summary, description
    118 
    119 
    120 def find_classifiers(docs):
    121     """
    122     Determine classifiers from CLASSIFIERS
    123 
    124     :return: List of classifiers (``['classifier', ...]``)
    125     :rtype: ``list``
    126     """
    127     filename = docs.get('meta.classifiers', 'CLASSIFIERS').strip()
    128     if filename and _os.path.isfile(filename):
    129         fp = open(filename, encoding='utf-8')
    130         try:
    131             content = fp.read()
    132         finally:
    133             fp.close()
    134         content = [item.strip() for item in content.splitlines()]
    135         return [item for item in content if item and not item.startswith('#')]
    136     return []
    137 
    138 
    139 def find_provides(docs):
    140     """
    141     Determine provides from PROVIDES
    142 
    143     :return: List of provides (``['provides', ...]``)
    144     :rtype: ``list``
    145     """
    146     filename = docs.get('meta.provides', 'PROVIDES').strip()
    147     if filename and _os.path.isfile(filename):
    148         fp = open(filename, encoding='utf-8')
    149         try:
    150             content = fp.read()
    151         finally:
    152             fp.close()
    153         content = [item.strip() for item in content.splitlines()]
    154         return [item for item in content if item and not item.startswith('#')]
    155     return []
    156 
    157 
    158 def find_license(docs):
    159     """
    160     Determine license from LICENSE
    161 
    162     :return: License text
    163     :rtype: ``str``
    164     """
    165     filename = docs.get('meta.license', 'LICENSE').strip()
    166     if filename and _os.path.isfile(filename):
    167         fp = open(filename, encoding='utf-8')
    168         try:
    169             return fp.read().rstrip()
    170         finally:
    171             fp.close()
    172     return None
    173 
    174 
    175 def find_packages(manifest):
    176     """ Determine packages and subpackages """
    177     packages = {}
    178     collect = manifest.get('packages.collect', '').split()
    179     lib = manifest.get('packages.lib', '.')
    180     try:
    181         sep = _os.path.sep
    182     except AttributeError:
    183         sep = _os.path.join('1', '2')[1:-1]
    184     for root in collect:
    185         for dirpath, _, filenames in _shell.walk(_os.path.join(lib, root)):
    186             if dirpath.find('.svn') >= 0 or dirpath.find('.git') >= 0:
    187                 continue
    188             if '__init__.py' in filenames:
    189                 packages[
    190                     _os.path.normpath(dirpath).replace(sep, '.')
    191                 ] = None
    192     packages = list(packages.keys())
    193     packages.sort()
    194     return packages
    195 
    196 
    197 def find_data(name, docs):
    198     """ Determine data files """
    199     result = []
    200     if docs.get('extra', '').strip():
    201         result.append(_data.Documentation(docs['extra'].split(),
    202             prefix='share/doc/%s' % name,
    203         ))
    204     if docs.get('examples.dir', '').strip():
    205         tpl = ['recursive-include %s *' % docs['examples.dir']]
    206         if docs.get('examples.ignore', '').strip():
    207             tpl.extend(["global-exclude %s" % item
    208                 for item in docs['examples.ignore'].split()
    209             ])
    210         strip = int(docs.get('examples.strip', '') or 0)
    211         result.append(_data.Documentation.from_templates(*tpl, **{
    212             'strip': strip,
    213             'prefix': 'share/doc/%s' % name,
    214             'preserve': 1,
    215         }))
    216     if docs.get('userdoc.dir', '').strip():
    217         tpl = ['recursive-include %s *' % docs['userdoc.dir']]
    218         if docs.get('userdoc.ignore', '').strip():
    219             tpl.extend(["global-exclude %s" % item
    220                 for item in docs['userdoc.ignore'].split()
    221             ])
    222         strip = int(docs.get('userdoc.strip', '') or 0)
    223         result.append(_data.Documentation.from_templates(*tpl, **{
    224             'strip': strip,
    225             'prefix': 'share/doc/%s' % name,
    226             'preserve': 1,
    227         }))
    228     if docs.get('apidoc.dir', '').strip():
    229         tpl = ['recursive-include %s *' % docs['apidoc.dir']]
    230         if docs.get('apidoc.ignore', '').strip():
    231             tpl.extend(["global-exclude %s" % item
    232                 for item in docs['apidoc.ignore'].split()
    233             ])
    234         strip = int(docs.get('apidoc.strip', '') or 0)
    235         result.append(_data.Documentation.from_templates(*tpl, **{
    236             'strip': strip,
    237             'prefix': 'share/doc/%s' % name,
    238             'preserve': 1,
    239         }))
    240     if docs.get('man', '').strip():
    241         result.extend(_data.Manpages.dispatch(docs['man'].split()))
    242     return result
    243 
    244 
    245 def make_manifest(manifest, config, docs, kwargs):
    246     """ Create file list to pack up """
    247     # pylint: disable = R0912
    248     kwargs = kwargs.copy()
    249     kwargs['script_args'] = ['install']
    250     kwargs['packages'] = list(kwargs.get('packages') or ()) + [
    251         '_setup', '_setup.py2', '_setup.py3',
    252     ] + list(manifest.get('packages.extra', '').split() or ())
    253     _core._setup_stop_after = "commandline"
    254     try:
    255         dist = _core.setup(**kwargs)
    256     finally:
    257         _core._setup_stop_after = None
    258 
    259     result = ['MANIFEST', 'PKG-INFO', 'setup.py'] + list(config)
    260     # TODO: work with default values:
    261     for key in ('classifiers', 'description', 'summary', 'provides',
    262                 'license'):
    263         filename = docs.get('meta.' + key, '').strip()
    264         if filename and _os.path.isfile(filename):
    265             result.append(filename)
    266 
    267     cmd = dist.get_command_obj("build_py")
    268     cmd.ensure_finalized()
    269     #from pprint import pprint; pprint(("build_py", cmd.get_source_files()))
    270     for item in cmd.get_source_files():
    271         result.append(_posixpath.sep.join(
    272             _os.path.normpath(item).split(_os.path.sep)
    273         ))
    274 
    275     cmd = dist.get_command_obj("build_ext")
    276     cmd.ensure_finalized()
    277     #from pprint import pprint; pprint(("build_ext", cmd.get_source_files()))
    278     for item in cmd.get_source_files():
    279         result.append(_posixpath.sep.join(
    280             _os.path.normpath(item).split(_os.path.sep)
    281         ))
    282     for ext in cmd.extensions:
    283         if ext.depends:
    284             result.extend([_posixpath.sep.join(
    285                 _os.path.normpath(item).split(_os.path.sep)
    286             ) for item in ext.depends])
    287 
    288     cmd = dist.get_command_obj("build_clib")
    289     cmd.ensure_finalized()
    290     if cmd.libraries:
    291         #import pprint; pprint.pprint(("build_clib", cmd.get_source_files()))
    292         for item in cmd.get_source_files():
    293             result.append(_posixpath.sep.join(
    294                 _os.path.normpath(item).split(_os.path.sep)
    295             ))
    296         for lib in cmd.libraries:
    297             if lib[1].get('depends'):
    298                 result.extend([_posixpath.sep.join(
    299                     _os.path.normpath(item).split(_os.path.sep)
    300                 ) for item in lib[1]['depends']])
    301 
    302     cmd = dist.get_command_obj("build_scripts")
    303     cmd.ensure_finalized()
    304     #import pprint; pprint.pprint(("build_scripts", cmd.get_source_files()))
    305     if cmd.get_source_files():
    306         for item in cmd.get_source_files():
    307             result.append(_posixpath.sep.join(
    308                 _os.path.normpath(item).split(_os.path.sep)
    309             ))
    310 
    311     cmd = dist.get_command_obj("install_data")
    312     cmd.ensure_finalized()
    313     #from pprint import pprint; pprint(("install_data", cmd.get_inputs()))
    314     try:
    315         strings = str
    316     except NameError:
    317         strings = (str, str)
    318 
    319     for item in cmd.get_inputs():
    320         if isinstance(item, strings):
    321             result.append(item)
    322         else:
    323             result.extend(item[1])
    324 
    325     for item in manifest.get('dist', '').split():
    326         result.append(item)
    327         if _os.path.isdir(item):
    328             for filename in _shell.files(item):
    329                 result.append(filename)
    330 
    331     result = list(dict([(item, None) for item in result]).keys())
    332     result.sort()
    333     return result
    334 
    335 
    336 def run(config=('package.cfg',), ext=None, script_args=None, manifest_only=0):
    337     """ Main runner """
    338     if ext is None:
    339         ext = []
    340 
    341     cfg = _util.SafeConfigParser()
    342     cfg.read(config, encoding='utf-8')
    343     pkg = dict(cfg.items('package'))
    344     python_min = pkg.get('python.min') or None
    345     python_max = pkg.get('python.max') or None
    346     check_python_version('python', python_min, python_max)
    347     pypy_min = pkg.get('pypy.min') or None
    348     pypy_max = pkg.get('pypy.max') or None
    349     check_python_version('pypy', pypy_min, pypy_max)
    350     jython_min = pkg.get('jython.min') or None
    351     jython_max = pkg.get('jython.max') or None
    352     check_python_version('jython', jython_min, jython_max)
    353 
    354     manifest = dict(cfg.items('manifest'))
    355     try:
    356         docs = dict(cfg.items('docs'))
    357     except _config_parser.NoSectionError:
    358         docs = {}
    359 
    360     summary, description = find_description(docs)
    361     scripts = manifest.get('scripts', '').strip() or None
    362     if scripts:
    363         scripts = scripts.split()
    364     modules = manifest.get('modules', '').strip() or None
    365     if modules:
    366         modules = modules.split()
    367     keywords = docs.get('meta.keywords', '').strip() or None
    368     if keywords:
    369         keywords = keywords.split()
    370     revision = pkg.get('version.revision', '').strip()
    371     if revision:
    372         revision = "-r%s" % (revision,)
    373 
    374     kwargs = {
    375         'name': pkg['name'],
    376         'version': "%s%s" % (
    377             pkg['version.number'],
    378             ["", "-dev%s" % (revision,)][_util.humanbool(
    379                 'version.dev', pkg.get('version.dev', 'false')
    380             )],
    381         ),
    382         'provides': find_provides(docs),
    383         'description': summary,
    384         'long_description': description,
    385         'classifiers': find_classifiers(docs),
    386         'keywords': keywords,
    387         'author': pkg['author.name'],
    388         'author_email': pkg['author.email'],
    389         'maintainer': pkg.get('maintainer.name'),
    390         'maintainer_email': pkg.get('maintainer.email'),
    391         'url': pkg.get('url.homepage'),
    392         'download_url': pkg.get('url.download'),
    393         'license': find_license(docs),
    394         'package_dir': {'': manifest.get('packages.lib', '.')},
    395         'packages': find_packages(manifest),
    396         'py_modules': modules,
    397         'ext_modules': ext,
    398         'scripts': scripts,
    399         'script_args': script_args,
    400         'data_files': find_data(pkg['name'], docs),
    401         'cmdclass': {
    402             'build'       : _commands.Build,
    403             'build_ext'   : _commands.BuildExt,
    404             'install'     : _commands.Install,
    405             'install_data': _commands.InstallData,
    406             'install_lib' : _commands.InstallLib,
    407         }
    408     }
    409     for key in ('provides',):
    410         if key not in _core.setup_keywords:
    411             del kwargs[key]
    412 
    413     if manifest_only:
    414         return make_manifest(manifest, config, docs, kwargs)
    415 
    416     # monkey-patch crappy manifest writer away.
    417     from distutils.command import sdist
    418     sdist.sdist.get_file_list = sdist.sdist.read_manifest
    419 
    420     return _core.setup(**kwargs)
    421