1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""Installs the OSKAR Python bindings."""
4
5import os
6import platform
7try:
8    from setuptools import setup, Extension
9    from setuptools.command.build_ext import build_ext
10except ImportError:
11    from distutils.core import setup, Extension
12    from distutils.command.build_ext import build_ext
13
14# Define the versions of OSKAR this is compatible with.
15OSKAR_COMPATIBILITY_VERSION_MIN = '0x020700'
16OSKAR_COMPATIBILITY_VERSION_MAX = '0x0208FF'
17
18# Define the extension modules to build.
19MODULES = [
20    ('_apps_lib', 'oskar_apps_lib.cpp'),
21    ('_binary_lib', 'oskar_binary_lib.c'),
22    ('_imager_lib', 'oskar_imager_lib.c'),
23    ('_measurement_set_lib', 'oskar_measurement_set_lib.c'),
24    ('_interferometer_lib', 'oskar_interferometer_lib.c'),
25    ('_settings_lib', 'oskar_settings_lib.cpp'),
26    ('_sky_lib', 'oskar_sky_lib.c'),
27    ('_telescope_lib', 'oskar_telescope_lib.c'),
28    ('_utils', 'oskar_utils.c'),
29    ('_vis_block_lib', 'oskar_vis_block_lib.c'),
30    ('_vis_header_lib', 'oskar_vis_header_lib.c'),
31    ('_bda_utils', 'oskar_bda_utils.c')
32]
33
34
35class BuildExt(build_ext):
36    """Class used to build OSKAR Python extensions. Inherits build_ext."""
37    def __init__(self, *args, **kwargs):
38        """Initialise."""
39        build_ext.__init__(self, *args, **kwargs)
40        self._checked_lib = False
41        self._checked_inc = False
42
43    @staticmethod
44    def find_file(name, dir_paths):
45        """Returns path of given file if it exists in the list of directories.
46
47        Args:
48            name (str):                   The name of the file to find.
49            dir_paths (array-like, str):  List of directories to search.
50        """
51        for directory in dir_paths:
52            directory = directory.strip('\"')
53            if os.path.exists(directory):
54                test_path = os.path.join(directory, name)
55                if os.path.isfile(test_path):
56                    return test_path.strip('\"')
57        return None
58
59    @staticmethod
60    def dir_contains(name, dir_paths):
61        """Returns directory if name fragment is part of a directory listing.
62
63        Args:
64            name (str):                   The name fragment to search for.
65            dir_paths (array-like, str):  List of directories to search.
66        """
67        for directory in dir_paths:
68            directory = directory.strip('\"')
69            if os.path.exists(directory):
70                dir_contents = os.listdir(directory)
71                for item in dir_contents:
72                    if name in item:
73                        return directory
74        return None
75
76    @staticmethod
77    def get_oskar_version(version_file):
78        """Returns the version of OSKAR found on the system."""
79        version_num = None
80        version_str = None
81        with open(version_file) as file_handle:
82            for line in file_handle:
83                if 'define OSKAR_VERSION_STR ' in line:
84                    version_str = (line.split()[2]).replace('"', '')
85                elif 'define OSKAR_VERSION ' in line:
86                    version_num = int(line.split()[2], base=16)
87        return (version_num, version_str)
88
89    @staticmethod
90    def check_oskar_version(version_num, version_str):
91        """Checks the version of OSKAR found is compatible."""
92        if version_num < int(OSKAR_COMPATIBILITY_VERSION_MIN, base=16) or \
93                version_num > int(OSKAR_COMPATIBILITY_VERSION_MAX, base=16):
94            raise RuntimeError(
95                "The version of OSKAR found is not compatible with oskarpy. "
96                "Found OSKAR %s (require %s < version < %s)." % (
97                    version_str,
98                    OSKAR_COMPATIBILITY_VERSION_MIN,
99                    OSKAR_COMPATIBILITY_VERSION_MAX)
100            )
101
102    def run(self):
103        """Overridden method. Runs the build.
104        Library directories and include directories are checked here, first.
105        """
106        # Check we can find the OSKAR library.
107        # For some reason, run() is sometimes called again after the build
108        # has already happened.
109        # Make sure not to fail the check the second time.
110        if not self._checked_lib:
111            self._checked_lib = True
112            if os.getenv('OSKAR_LIB_DIR'):
113                self.library_dirs.append(os.getenv('OSKAR_LIB_DIR'))
114            if platform.system() == 'Windows':
115                self.library_dirs.append('C:\\Program Files\\OSKAR\\lib')
116            for i, test_dir in enumerate(self.library_dirs):
117                self.library_dirs[i] = test_dir.strip('\"')
118            directory = self.dir_contains('oskar.', self.library_dirs)
119            if not directory:
120                raise RuntimeError(
121                    "Could not find OSKAR library. "
122                    "Check that OSKAR has already been installed on "
123                    "this system, and either set the environment variable "
124                    "OSKAR_LIB_DIR, or set the library path to build_ext "
125                    "using -L or --library-dirs")
126            if platform.system() != 'Windows':
127                self.rpath.append(directory)
128            self.libraries.append('oskar')
129            self.libraries.append('oskar_apps')
130            self.libraries.append('oskar_binary')
131            self.libraries.append('oskar_settings')
132            if self.dir_contains('oskar_ms.', self.library_dirs):
133                self.libraries.append('oskar_ms')
134
135        # Check we can find the OSKAR headers.
136        if not self._checked_inc:
137            from numpy import get_include
138            self._checked_inc = True
139            if os.getenv('OSKAR_INC_DIR'):
140                self.include_dirs.append(os.getenv('OSKAR_INC_DIR'))
141            if platform.system() == 'Windows':
142                self.include_dirs.append('C:\\Program Files\\OSKAR\\include')
143            header = self.find_file(
144                os.path.join('oskar', 'oskar_version.h'), self.include_dirs)
145            if not header:
146                raise RuntimeError(
147                    "Could not find oskar/oskar_version.h. "
148                    "Check that OSKAR has already been installed on "
149                    "this system, and either set the environment variable "
150                    "OSKAR_INC_DIR, or set the include path to build_ext "
151                    "using -I or --include-dirs")
152            self.include_dirs.insert(0, os.path.dirname(header))
153            self.include_dirs.insert(0, get_include())
154            for i, test_dir in enumerate(self.include_dirs):
155                self.include_dirs[i] = test_dir.strip('\"')
156
157            # Check the version of OSKAR is compatible.
158            version = self.get_oskar_version(header)
159            self.check_oskar_version(*version)
160        build_ext.run(self)
161
162    def build_extension(self, ext):
163        """Overridden method. Builds each Extension."""
164        ext.runtime_library_dirs = self.rpath
165
166        # Unfortunately things don't work as they should on the Mac...
167        if platform.system() == 'Darwin':
168            for rpath in self.rpath:
169                ext.extra_link_args.append('-Wl,-rpath,'+rpath)
170
171        # Don't try to build MS extension if liboskar_ms is not found.
172        if 'measurement_set' in ext.name:
173            if not self.dir_contains('oskar_ms.', self.library_dirs):
174                return
175        build_ext.build_extension(self, ext)
176
177
178def get_oskarpy_version():
179    """Get the version of oskarpy from the version file."""
180    globals_ = {}
181    this_dir = os.path.dirname(__file__)
182    with open(os.path.join(this_dir, 'oskar', '_version.py')) as file_handle:
183        code = file_handle.read()
184    # pylint: disable=exec-used
185    exec(code, globals_)
186    return globals_['__version__']
187
188
189# Call setup() with list of extensions to build.
190EXTENSIONS = []
191for module in MODULES:
192    if platform.system() == 'Windows' and 'measurement_set' in module[0]:
193        continue
194    _, src_ext = os.path.splitext(module[1])
195    extra_compile_args = []
196    if src_ext == ".c" and platform.system() != 'Windows':
197        extra_compile_args = ["-std=c99"]
198    EXTENSIONS.append(Extension(
199        'oskar.' + module[0],
200        sources=[os.path.join('oskar', 'src', module[1])],
201        extra_compile_args=extra_compile_args))
202setup(
203    name='oskarpy',
204    version=get_oskarpy_version(),
205    description='Radio interferometer simulation package (Python bindings)',
206    packages=['oskar'],
207    ext_modules=EXTENSIONS,
208    classifiers=[
209        'Development Status :: 3 - Alpha',
210        'Environment :: Console',
211        'Intended Audience :: Science/Research',
212        'Topic :: Scientific/Engineering :: Astronomy',
213        'License :: OSI Approved :: BSD License',
214        'Operating System :: POSIX',
215        'Operating System :: MacOS :: MacOS X',
216        'Operating System :: Microsoft :: Windows',
217        'Programming Language :: C',
218        'Programming Language :: Python :: 2.7',
219        'Programming Language :: Python :: 3'
220    ],
221    author='University of Oxford',
222    url='https://github.com/OxfordSKA/OSKAR',
223    license='BSD',
224    install_requires=['numpy'],
225    setup_requires=['numpy'],
226    cmdclass={'build_ext': BuildExt}
227    )
228