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
22This module provides a wrapper around the distutils core setup.
23"""
24__author__ = "Andr\xe9 Malo"
25__docformat__ = "restructuredtext en"
26
27import configparser as _config_parser
28from distutils import core as _core
29import os as _os
30import posixpath as _posixpath
31import sys as _sys
32
33from _setup import commands as _commands
34from _setup import data as _data
35from _setup import ext as _ext
36from _setup import util as _util
37from _setup import shell as _shell
38
39
40def 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 == 'pypy3':
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', 'pypy3', '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
75def 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
120def 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
139def 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
158def 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
175def 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
197def 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
245def 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"  # noqa pylint: disable = protected-access
254    try:
255        dist = _core.setup(**kwargs)
256    finally:
257        _core._setup_stop_after = None  # pylint: disable = protected-access
258
259    result = ['MANIFEST', 'PKG-INFO', 'setup.py'] + list(config)
260    # xx: 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
336def run(config=('package.cfg',), ext=None, script_args=None, manifest_only=0):
337    """ Main runner """
338    # pylint: disable = too-many-locals
339    if ext is None:
340        ext = []
341
342    cfg = _util.SafeConfigParser()
343    if (3, 0, 0) <= _sys.version_info < (3, 2, 0):
344        cfg.read(config)
345    else:
346        cfg.read(config, encoding='utf-8')
347    pkg = dict(cfg.items('package'))
348    python_min = pkg.get('python.min') or None
349    python_max = pkg.get('python.max') or None
350    check_python_version('python', python_min, python_max)
351    pypy_min = pkg.get('pypy3.min') or None
352    pypy_max = pkg.get('pypy3.max') or None
353    check_python_version('pypy3', pypy_min, pypy_max)
354    jython_min = pkg.get('jython.min') or None
355    jython_max = pkg.get('jython.max') or None
356    check_python_version('jython', jython_min, jython_max)
357
358    manifest = dict(cfg.items('manifest'))
359    try:
360        docs = dict(cfg.items('docs'))
361    except _config_parser.NoSectionError:
362        docs = {}
363
364    summary, description = find_description(docs)
365    scripts = manifest.get('scripts', '').strip() or None
366    if scripts:
367        scripts = scripts.split()
368    modules = manifest.get('modules', '').strip() or None
369    if modules:
370        modules = modules.split()
371    keywords = docs.get('meta.keywords', '').strip() or None
372    if keywords:
373        keywords = keywords.split()
374    revision = pkg.get('version.revision', '').strip()
375    if revision:
376        revision = int(revision)
377    else:
378        revision = 0
379
380    kwargs = {
381        'name': pkg['name'],
382        'version': "%s%s" % (
383            pkg['version.number'],
384            ["", ".dev%d" % (revision,)][_util.humanbool(
385                'version.dev', pkg.get('version.dev', 'false')
386            )],
387        ),
388        'provides': find_provides(docs),
389        'description': summary,
390        'long_description': description,
391        'classifiers': find_classifiers(docs),
392        'keywords': keywords,
393        'author': pkg['author.name'],
394        'author_email': pkg['author.email'],
395        'maintainer': pkg.get('maintainer.name'),
396        'maintainer_email': pkg.get('maintainer.email'),
397        'url': pkg.get('url.homepage'),
398        'download_url': pkg.get('url.download'),
399        'license': find_license(docs),
400        'package_dir': {'': manifest.get('packages.lib', '.')},
401        'packages': find_packages(manifest),
402        'py_modules': modules,
403        'ext_modules': ext,
404        'scripts': scripts,
405        'script_args': script_args,
406        'data_files': find_data(pkg['name'], docs),
407        'cmdclass': {
408            'build'       : _commands.Build,
409            'build_ext'   : _commands.BuildExt,
410            'install'     : _commands.Install,
411            'install_data': _commands.InstallData,
412            'install_lib' : _commands.InstallLib,
413        }
414    }
415    for key in ('provides',):
416        if key not in _core.setup_keywords:
417            del kwargs[key]
418
419    if manifest_only:
420        return make_manifest(manifest, config, docs, kwargs)
421
422    # monkey-patch crappy manifest writer away.
423    from distutils.command import sdist
424    sdist.sdist.get_file_list = sdist.sdist.read_manifest
425
426    return _core.setup(**kwargs)
427