1#!/usr/bin/env python
2# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
3# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
4#
5# MDAnalysis --- https://www.mdanalysis.org
6# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
7# (see the file AUTHORS for the full list of names)
8#
9# Released under the GNU Public Licence, v2 or any higher version
10#
11# Please cite your use of MDAnalysis in published work:
12#
13# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
14# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
15# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
16# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
17# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
18#
19# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
20# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
21# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
22#
23
24"""Setuptools-based setup script for MDAnalysis.
25
26A working installation of NumPy <http://numpy.scipy.org> is required.
27
28For a basic installation just type the command::
29
30  python setup.py install
31
32For more in-depth instructions, see the installation section at the
33MDAnalysis Wiki:
34
35  https://github.com/MDAnalysis/mdanalysis/wiki/INSTALL
36
37Also free to ask on the MDAnalysis mailing list for help:
38
39  http://groups.google.com/group/mdnalysis-discussion
40
41(Note that the group really is called `mdnalysis-discussion' because
42Google groups forbids any name that contains the string `anal'.)
43"""
44
45from __future__ import print_function
46from setuptools import setup, Extension, find_packages
47from distutils.ccompiler import new_compiler
48import codecs
49import os
50import sys
51import shutil
52import tempfile
53import warnings
54import platform
55
56# Make sure I have the right Python version.
57if sys.version_info[:2] < (2, 7):
58    print('MDAnalysis requires Python 2.7 or better. Python {0:d}.{1:d} detected'.format(*
59          sys.version_info[:2]))
60    print('Please upgrade your version of Python.')
61    sys.exit(-1)
62
63if sys.version_info[0] < 3:
64    import ConfigParser as configparser
65else:
66    import configparser
67
68
69# NOTE: keep in sync with MDAnalysis.__version__ in version.py
70RELEASE = "0.19.2"
71
72is_release = 'dev' not in RELEASE
73
74# Handle cython modules
75try:
76    # cython has to be >=0.16 <0.28 to support cython.parallel
77    import Cython
78    from Cython.Build import cythonize
79    cython_found = True
80    from distutils.version import LooseVersion
81
82    required_version = "0.16"
83    if not LooseVersion(Cython.__version__) >= LooseVersion(required_version):
84        # We don't necessarily die here. Maybe we already have
85        #  the cythonized '.c' files.
86        print("Cython version {0} was found but won't be used: version {1} "
87              "or greater is required because it offers a handy "
88              "parallelization module".format(
89               Cython.__version__, required_version))
90        cython_found = False
91except ImportError:
92    cython_found = False
93    if not is_release:
94        print("*** package: Cython not found ***")
95        print("MDAnalysis requires cython for development builds")
96        sys.exit(1)
97
98
99class Config(object):
100    """Config wrapper class to get build options
101
102    This class looks for options in the environment variables and the
103    'setup.cfg' file. The order how we look for an option is.
104
105    1. Environment Variable
106    2. set in 'setup.cfg'
107    3. given default
108
109    Environment variables should start with 'MDA_' and be all uppercase.
110    Values passed to environment variables are checked (case-insensitively)
111    for specific strings with boolean meaning: 'True' or '1' will cause `True`
112    to be returned. '0' or 'False' cause `False` to be returned.
113
114    """
115
116    def __init__(self, fname='setup.cfg'):
117        if os.path.exists(fname):
118            self.config = configparser.SafeConfigParser()
119            self.config.read(fname)
120
121    def get(self, option_name, default=None):
122        environ_name = 'MDA_' + option_name.upper()
123        if environ_name in os.environ:
124            val = os.environ[environ_name]
125            if val.upper() in ('1', 'TRUE'):
126                return True
127            elif val.upper() in ('0', 'FALSE'):
128                return False
129            return val
130        try:
131            option = self.config.get('options', option_name)
132            return option
133        except configparser.NoOptionError:
134            return default
135
136
137class MDAExtension(Extension, object):
138    """Derived class to cleanly handle setup-time (numpy) dependencies.
139    """
140    # The only setup-time numpy dependency comes when setting up its
141    #  include dir.
142    # The actual numpy import and call can be delayed until after pip
143    #  has figured it must install numpy.
144    # This is accomplished by passing the get_numpy_include function
145    #  as one of the include_dirs. This derived Extension class takes
146    #  care of calling it when needed.
147    def __init__(self, *args, **kwargs):
148        self._mda_include_dirs = []
149        super(MDAExtension, self).__init__(*args, **kwargs)
150
151    @property
152    def include_dirs(self):
153        if not self._mda_include_dirs:
154            for item in self._mda_include_dir_args:
155                try:
156                    self._mda_include_dirs.append(item()) #The numpy callable
157                except TypeError:
158                    self._mda_include_dirs.append(item)
159        return self._mda_include_dirs
160
161    @include_dirs.setter
162    def include_dirs(self, val):
163        self._mda_include_dir_args = val
164
165
166def get_numpy_include():
167    # Obtain the numpy include directory. This logic works across numpy
168    # versions.
169    # setuptools forgets to unset numpy's setup flag and we get a crippled
170    # version of it unless we do it ourselves.
171    try:
172        # Python 3 renamed the ``__builin__`` module into ``builtins``.
173        # Here we import the python 2 or the python 3 version of the module
174        # with the python 3 name. This could be done with ``six`` but that
175        # module may not be installed at that point.
176        import builtins
177    except ImportError:
178        import __builtin__ as builtins
179    builtins.__NUMPY_SETUP__ = False
180    try:
181        import numpy as np
182    except ImportError:
183        print('*** package "numpy" not found ***')
184        print('MDAnalysis requires a version of NumPy (>=1.10.4), even for setup.')
185        print('Please get it from http://numpy.scipy.org/ or install it through '
186              'your package manager.')
187        sys.exit(-1)
188    return np.get_include()
189
190
191def hasfunction(cc, funcname, include=None, extra_postargs=None):
192    # From http://stackoverflow.com/questions/
193    #            7018879/disabling-output-when-compiling-with-distutils
194    tmpdir = tempfile.mkdtemp(prefix='hasfunction-')
195    devnull = oldstderr = None
196    try:
197        try:
198            fname = os.path.join(tmpdir, 'funcname.c')
199            with open(fname, 'w') as f:
200                if include is not None:
201                    f.write('#include {0!s}\n'.format(include))
202                f.write('int main(void) {\n')
203                f.write('    {0!s};\n'.format(funcname))
204                f.write('}\n')
205            # Redirect stderr to /dev/null to hide any error messages
206            # from the compiler.
207            # This will have to be changed if we ever have to check
208            # for a function on Windows.
209            devnull = open('/dev/null', 'w')
210            oldstderr = os.dup(sys.stderr.fileno())
211            os.dup2(devnull.fileno(), sys.stderr.fileno())
212            objects = cc.compile([fname], output_dir=tmpdir,
213                                 extra_postargs=extra_postargs)
214            cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
215        except Exception:
216            return False
217        return True
218    finally:
219        if oldstderr is not None:
220            os.dup2(oldstderr, sys.stderr.fileno())
221        if devnull is not None:
222            devnull.close()
223        shutil.rmtree(tmpdir)
224
225
226def detect_openmp():
227    """Does this compiler support OpenMP parallelization?"""
228    print("Attempting to autodetect OpenMP support... ", end="")
229    compiler = new_compiler()
230    compiler.add_library('gomp')
231    include = '<omp.h>'
232    extra_postargs = ['-fopenmp']
233    hasopenmp = hasfunction(compiler, 'omp_get_num_threads()', include=include,
234                            extra_postargs=extra_postargs)
235    if hasopenmp:
236        print("Compiler supports OpenMP")
237    else:
238        print("Did not detect OpenMP support.")
239    return hasopenmp
240
241
242def extensions(config):
243    # dev installs must build their own cythonized files.
244    use_cython = config.get('use_cython', default=not is_release)
245    use_openmp = config.get('use_openmp', default=True)
246
247    extra_compile_args = ['-std=c99', '-ffast-math', '-funroll-loops']
248    define_macros = []
249    if config.get('debug_cflags', default=False):
250        extra_compile_args.extend(['-Wall', '-pedantic'])
251        define_macros.extend([('DEBUG', '1')])
252
253    # allow using architecture specific instructions. This allows people to
254    # build optimized versions of MDAnalysis.
255    arch = config.get('march', default=False)
256    if arch:
257        extra_compile_args.append('-march={}'.format(arch))
258
259    cpp_extra_compile_args = [a for a in extra_compile_args if 'std' not in a]
260    cpp_extra_compile_args.append('-std=c++11')
261    # needed to specify c++ runtime library on OSX
262    if platform.system() == 'Darwin':
263        cpp_extra_compile_args.append('-stdlib=libc++')
264
265    # Needed for large-file seeking under 32bit systems (for xtc/trr indexing
266    # and access).
267    largefile_macros = [
268        ('_LARGEFILE_SOURCE', None),
269        ('_LARGEFILE64_SOURCE', None),
270        ('_FILE_OFFSET_BITS', '64')
271    ]
272
273    has_openmp = detect_openmp()
274
275    if use_openmp and not has_openmp:
276        print('No openmp compatible compiler found default to serial build.')
277
278    parallel_args = ['-fopenmp'] if has_openmp and use_openmp else []
279    parallel_libraries = ['gomp'] if has_openmp and use_openmp else []
280    parallel_macros = [('PARALLEL', None)] if has_openmp and use_openmp else []
281
282    if use_cython:
283        print('Will attempt to use Cython.')
284        if not cython_found:
285            print("Couldn't find a Cython installation. "
286                  "Not recompiling cython extensions.")
287            use_cython = False
288    else:
289        print('Will not attempt to use Cython.')
290
291    source_suffix = '.pyx' if use_cython else '.c'
292    cpp_source_suffix = '.pyx' if use_cython else '.cpp'
293
294    # The callable is passed so that it is only evaluated at install time.
295
296    include_dirs = [get_numpy_include]
297    # Windows automatically handles math library linking
298    # and will not build MDAnalysis if we try to specify one
299    if os.name == 'nt':
300        mathlib = []
301    else:
302        mathlib = ['m']
303
304    libdcd = MDAExtension('MDAnalysis.lib.formats.libdcd',
305                          ['MDAnalysis/lib/formats/libdcd' + source_suffix],
306                          include_dirs=include_dirs + ['MDAnalysis/lib/formats/include'],
307                          define_macros=define_macros,
308                          extra_compile_args=extra_compile_args)
309    distances = MDAExtension('MDAnalysis.lib.c_distances',
310                             ['MDAnalysis/lib/c_distances' + source_suffix],
311                             include_dirs=include_dirs + ['MDAnalysis/lib/include'],
312                             libraries=mathlib,
313                             define_macros=define_macros,
314                             extra_compile_args=extra_compile_args)
315    distances_omp = MDAExtension('MDAnalysis.lib.c_distances_openmp',
316                                 ['MDAnalysis/lib/c_distances_openmp' + source_suffix],
317                                 include_dirs=include_dirs + ['MDAnalysis/lib/include'],
318                                 libraries=mathlib + parallel_libraries,
319                                 define_macros=define_macros + parallel_macros,
320                                 extra_compile_args=parallel_args + extra_compile_args,
321                                 extra_link_args=parallel_args)
322    qcprot = MDAExtension('MDAnalysis.lib.qcprot',
323                          ['MDAnalysis/lib/qcprot' + source_suffix],
324                          include_dirs=include_dirs,
325                          define_macros=define_macros,
326                          extra_compile_args=extra_compile_args)
327    transformation = MDAExtension('MDAnalysis.lib._transformations',
328                                  ['MDAnalysis/lib/src/transformations/transformations.c'],
329                                  libraries=mathlib,
330                                  define_macros=define_macros,
331                                  include_dirs=include_dirs,
332                                  extra_compile_args=extra_compile_args)
333    libmdaxdr = MDAExtension('MDAnalysis.lib.formats.libmdaxdr',
334                             sources=['MDAnalysis/lib/formats/libmdaxdr' + source_suffix,
335                                      'MDAnalysis/lib/formats/src/xdrfile.c',
336                                      'MDAnalysis/lib/formats/src/xdrfile_xtc.c',
337                                      'MDAnalysis/lib/formats/src/xdrfile_trr.c',
338                                      'MDAnalysis/lib/formats/src/trr_seek.c',
339                                      'MDAnalysis/lib/formats/src/xtc_seek.c',
340                             ],
341                             include_dirs=include_dirs + ['MDAnalysis/lib/formats/include',
342                                                          'MDAnalysis/lib/formats'],
343                             define_macros=largefile_macros + define_macros,
344                             extra_compile_args=extra_compile_args)
345    util = MDAExtension('MDAnalysis.lib.formats.cython_util',
346                        sources=['MDAnalysis/lib/formats/cython_util' + source_suffix],
347                        include_dirs=include_dirs,
348                        define_macros=define_macros,
349                        extra_compile_args=extra_compile_args)
350    cutil = MDAExtension('MDAnalysis.lib._cutil',
351                         sources=['MDAnalysis/lib/_cutil' + cpp_source_suffix],
352                         language='c++',
353                         libraries=mathlib,
354                         include_dirs=include_dirs + ['MDAnalysis/lib/include'],
355                         define_macros=define_macros,
356                         extra_compile_args=cpp_extra_compile_args)
357    augment = MDAExtension('MDAnalysis.lib._augment',
358                         sources=['MDAnalysis/lib/_augment' + cpp_source_suffix],
359                         language='c++',
360                         include_dirs=include_dirs,
361                         define_macros=define_macros,
362                         extra_compile_args=cpp_extra_compile_args)
363
364
365    encore_utils = MDAExtension('MDAnalysis.analysis.encore.cutils',
366                                sources=['MDAnalysis/analysis/encore/cutils' + source_suffix],
367                                include_dirs=include_dirs,
368                                define_macros=define_macros,
369                                extra_compile_args=extra_compile_args)
370    ap_clustering = MDAExtension('MDAnalysis.analysis.encore.clustering.affinityprop',
371                                 sources=['MDAnalysis/analysis/encore/clustering/affinityprop' + source_suffix,
372                                          'MDAnalysis/analysis/encore/clustering/src/ap.c'],
373                                 include_dirs=include_dirs+['MDAnalysis/analysis/encore/clustering/include'],
374                                 libraries=mathlib,
375                                 define_macros=define_macros,
376                                 extra_compile_args=extra_compile_args)
377    spe_dimred = MDAExtension('MDAnalysis.analysis.encore.dimensionality_reduction.stochasticproxembed',
378                              sources=['MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed' + source_suffix,
379                                       'MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c'],
380                              include_dirs=include_dirs+['MDAnalysis/analysis/encore/dimensionality_reduction/include'],
381                              libraries=mathlib,
382                              define_macros=define_macros,
383                              extra_compile_args=extra_compile_args)
384    nsgrid = MDAExtension('MDAnalysis.lib.nsgrid',
385                             ['MDAnalysis/lib/nsgrid' + cpp_source_suffix],
386                             include_dirs=include_dirs,
387                             language='c++',
388                             define_macros=define_macros,
389                             extra_compile_args=cpp_extra_compile_args)
390    pre_exts = [libdcd, distances, distances_omp, qcprot,
391                transformation, libmdaxdr, util, encore_utils,
392                ap_clustering, spe_dimred, cutil, augment, nsgrid]
393
394
395    cython_generated = []
396    if use_cython:
397        extensions = cythonize(pre_exts)
398        for pre_ext, post_ext in zip(pre_exts, extensions):
399            for source in post_ext.sources:
400                if source not in pre_ext.sources:
401                    cython_generated.append(source)
402    else:
403        #Let's check early for missing .c files
404        extensions = pre_exts
405        for ext in extensions:
406            for source in ext.sources:
407                if not (os.path.isfile(source) and
408                        os.access(source, os.R_OK)):
409                    raise IOError("Source file '{}' not found. This might be "
410                                "caused by a missing Cython install, or a "
411                                "failed/disabled Cython build.".format(source))
412    return extensions, cython_generated
413
414
415def dynamic_author_list():
416    """Generate __authors__ from AUTHORS
417
418    This function generates authors.py that contains the list of the
419    authors from the AUTHORS file. This avoids having that list maintained in
420    several places. Note that AUTHORS is sorted chronologically while we want
421    __authors__ in authors.py to be sorted alphabetically.
422
423    The authors are written in AUTHORS as bullet points under the
424    "Chronological list of authors" title.
425    """
426    authors = []
427    with codecs.open('AUTHORS', encoding='utf-8') as infile:
428        # An author is a bullet point under the title "Chronological list of
429        # authors". We first want move the cursor down to the title of
430        # interest.
431        for line_no, line in enumerate(infile, start=1):
432            if line.rstrip() == "Chronological list of authors":
433                break
434        else:
435            # If we did not break, it means we did not find the authors.
436            raise IOError('EOF before the list of authors')
437        # Skip the next line as it is the title underlining
438        line = next(infile)
439        line_no += 1
440        if line[:4] != '----':
441            raise IOError('Unexpected content on line {0}, '
442                          'should be a string of "-".'.format(line_no))
443        # Add each bullet point as an author until the next title underlining
444        for line in infile:
445            if line[:4] in ('----', '====', '~~~~'):
446                # The previous line was a title, hopefully it did not start as
447                # a bullet point so it got ignored. Since we hit a title, we
448                # are done reading the list of authors.
449                break
450            elif line.strip()[:2] == '- ':
451                # This is a bullet point, so it should be an author name.
452                name = line.strip()[2:].strip()
453                authors.append(name)
454
455    # So far, the list of authors is sorted chronologically. We want it
456    # sorted alphabetically of the last name.
457    authors.sort(key=lambda name: name.split()[-1])
458    # Move Naveen and Elizabeth first, and Oliver last.
459    authors.remove('Naveen Michaud-Agrawal')
460    authors.remove('Elizabeth J. Denning')
461    authors.remove('Oliver Beckstein')
462    authors = (['Naveen Michaud-Agrawal', 'Elizabeth J. Denning']
463               + authors + ['Oliver Beckstein'])
464
465    # Write the authors.py file.
466    out_path = 'MDAnalysis/authors.py'
467    with codecs.open(out_path, 'w', encoding='utf-8') as outfile:
468        # Write the header
469        header = '''\
470#-*- coding:utf-8 -*-
471
472# This file is generated from the AUTHORS file during the installation process.
473# Do not edit it as your changes will be overwritten.
474'''
475        print(header, file=outfile)
476
477        # Write the list of authors as a python list
478        template = u'__authors__ = [\n{}\n]'
479        author_string = u',\n'.join(u'    u"{}"'.format(name)
480                                    for name in authors)
481        print(template.format(author_string), file=outfile)
482
483
484if __name__ == '__main__':
485    try:
486        dynamic_author_list()
487    except (OSError, IOError):
488        warnings.warn('Cannot write the list of authors.')
489
490    with open("SUMMARY.txt") as summary:
491        LONG_DESCRIPTION = summary.read()
492    CLASSIFIERS = [
493        'Development Status :: 4 - Beta',
494        'Environment :: Console',
495        'Intended Audience :: Science/Research',
496        'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
497        'Operating System :: POSIX',
498        'Operating System :: MacOS :: MacOS X',
499        'Operating System :: Microsoft :: Windows ',
500        'Programming Language :: Python',
501        'Programming Language :: Python :: 2',
502        'Programming Language :: Python :: 2.7',
503        'Programming Language :: Python :: 3',
504        'Programming Language :: Python :: 3.4',
505        'Programming Language :: Python :: 3.5',
506        'Programming Language :: Python :: 3.6',
507        'Programming Language :: C',
508        'Topic :: Scientific/Engineering',
509        'Topic :: Scientific/Engineering :: Bio-Informatics',
510        'Topic :: Scientific/Engineering :: Chemistry',
511        'Topic :: Software Development :: Libraries :: Python Modules',
512    ]
513    config = Config()
514    exts, cythonfiles = extensions(config)
515
516    install_requires = [
517          'numpy>=1.10.4',
518          'biopython>=1.71',
519          'networkx>=1.0',
520          'GridDataFormats>=0.4.0',
521          'six>=1.4.0',
522          'mmtf-python>=1.0.0',
523          'joblib',
524          'scipy>=1.0.0',
525          'matplotlib>=1.5.1',
526          'mock',
527    ]
528    if not os.name == 'nt':
529        install_requires.append('gsd>=1.4.0')
530
531    setup(name='MDAnalysis',
532          version=RELEASE,
533          description=('An object-oriented toolkit to analyze molecular dynamics '
534                       'trajectories generated by CHARMM, Gromacs, NAMD, LAMMPS, or Amber.'),
535          long_description=LONG_DESCRIPTION,
536          long_description_content_type='text/x-rst',
537          author='Naveen Michaud-Agrawal',
538          author_email='naveen.michaudagrawal@gmail.com',
539          maintainer='Richard Gowers',
540          maintainer_email='mdnalysis-discussion@googlegroups.com',
541          url='https://www.mdanalysis.org',
542          download_url='https://github.com/MDAnalysis/mdanalysis/releases',
543          project_urls={'Documentation': 'https://www.mdanalysis.org/docs/',
544                        'Issue Tracker': 'https://github.com/mdanalysis/mdanalysis/issues',
545                        'User Group': 'https://groups.google.com/forum/#!forum/mdnalysis-discussion',
546                        'Source': 'https://github.com/mdanalysis/mdanalysis',
547                        },
548          license='GPL 2',
549          classifiers=CLASSIFIERS,
550          provides=['MDAnalysis'],
551          packages=find_packages(),
552          package_data={'MDAnalysis':
553                        ['analysis/data/*.npy',
554                        ],
555          },
556          ext_modules=exts,
557          requires=['numpy (>=1.10.4)', 'biopython (>= 1.71)', 'mmtf (>=1.0.0)',
558                    'networkx (>=1.0)', 'GridDataFormats (>=0.3.2)', 'joblib',
559                    'scipy (>=1.0.0)', 'matplotlib (>=1.5.1)'],
560          # all standard requirements are available through PyPi and
561          # typically can be installed without difficulties through setuptools
562          setup_requires=[
563              'numpy>=1.10.4',
564          ],
565          install_requires=install_requires,
566          # extras can be difficult to install through setuptools and/or
567          # you might prefer to use the version available through your
568          # packaging system
569          extras_require={
570              'AMBER': [
571                  'netCDF4>=1.0',  # for fast AMBER writing, also needs HDF5
572              ],
573              'analysis': [
574                  'seaborn',  # for annotated heat map and nearest neighbor
575                              # plotting in PSA
576                  'sklearn',  # For clustering and dimensionality reduction
577                              # functionality in encore
578              ],
579          },
580          test_suite="MDAnalysisTests",
581          tests_require=[
582              'MDAnalysisTests=={0!s}'.format(RELEASE),  # same as this release!
583          ],
584          zip_safe=False,  # as a zipped egg the *.so files are not found (at
585                           # least in Ubuntu/Linux)
586    )
587
588    # Releases keep their cythonized stuff for shipping.
589    if not config.get('keep_cythonized', default=is_release):
590        for cythonized in cythonfiles:
591            try:
592                os.unlink(cythonized)
593            except OSError as err:
594                print("Warning: failed to delete cythonized file {0}: {1}. "
595                    "Moving on.".format(cythonized, err.strerror))
596