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