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__ = u"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 == '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
74def 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
119def 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
138def 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
157def 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
174def 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
196def 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
244def 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
335def 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 = "-r%s" % (revision,)
372
373    kwargs = {
374        'name': pkg['name'],
375        'version': "%s%s" % (
376            pkg['version.number'],
377            ["", "-dev%s" % (revision,)][_util.humanbool(
378                'version.dev', pkg.get('version.dev', 'false')
379            )],
380        ),
381        'provides': find_provides(docs),
382        'description': summary,
383        'long_description': description,
384        'classifiers': find_classifiers(docs),
385        'keywords': keywords,
386        'author': pkg['author.name'],
387        'author_email': pkg['author.email'],
388        'maintainer': pkg.get('maintainer.name'),
389        'maintainer_email': pkg.get('maintainer.email'),
390        'url': pkg.get('url.homepage'),
391        'download_url': pkg.get('url.download'),
392        'license': find_license(docs),
393        'package_dir': {'': manifest.get('packages.lib', '.')},
394        'packages': find_packages(manifest),
395        'py_modules': modules,
396        'ext_modules': ext,
397        'scripts': scripts,
398        'script_args': script_args,
399        'data_files': find_data(pkg['name'], docs),
400        'cmdclass': {
401            'build'       : _commands.Build,
402            'build_ext'   : _commands.BuildExt,
403            'install'     : _commands.Install,
404            'install_data': _commands.InstallData,
405            'install_lib' : _commands.InstallLib,
406        }
407    }
408    for key in ('provides',):
409        if key not in _core.setup_keywords:
410            del kwargs[key]
411
412    if manifest_only:
413        return make_manifest(manifest, config, docs, kwargs)
414
415    # monkey-patch crappy manifest writer away.
416    from distutils.command import sdist
417    sdist.sdist.get_file_list = sdist.sdist.read_manifest
418
419    return _core.setup(**kwargs)
420