setup.py 11.5 KB
Newer Older
1

Simon Ward's avatar
Simon Ward committed
2
3
4
5
6
7
import os
import pathlib
import pkgutil
import shutil
import struct
import sys
Simon Ward's avatar
Simon Ward committed
8
import setuptools
Simon Ward's avatar
Simon Ward committed
9
import distutils.sysconfig as sysconfig
Simon Ward's avatar
Simon Ward committed
10
from distutils.core import setup
11
import distutils.command.build
Simon Ward's avatar
Simon Ward committed
12
from distutils.command.install_data import install_data
Simon Ward's avatar
Simon Ward committed
13
14
from subprocess import CalledProcessError, check_output, check_call

Simon Ward's avatar
Simon Ward committed
15
16
17
18
19
from setuptools import find_packages, setup, Extension
from setuptools.command.build_ext import build_ext
from setuptools.command.install_lib import install_lib
from setuptools.command.install_scripts import install_scripts

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
try:
    from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
    # this overrides standard naming of the wheel to not include
    # architecture or python dot version number

    class Bdist_wheel(_bdist_wheel):
        def finalize_options(self):
            _bdist_wheel.finalize_options(self)
            self.root_is_pure = False

        def get_tag(self):
            python, abi, plat = _bdist_wheel.get_tag(self)
            python, abi = 'py3', 'none'
            return python, abi, plat
except ImportError:
    Bdist_wheel = None

Simon Ward's avatar
Simon Ward committed
37
38
39
40
BITS = struct.calcsize("P") * 8
PACKAGE_NAME = "crysfml_api"
SOURCE_DIR = '.'
COMPILER = 'gfortran'
Simon Ward's avatar
Simon Ward committed
41
42
43
if os.environ.get('FC', False):
    COMPILER = os.environ.get('FC')
print(f'Compiler set to: {COMPILER}')
Simon Ward's avatar
Simon Ward committed
44

Simon Ward's avatar
Simon Ward committed
45
46
47
48
49
50
# We can use cmake provided from pip which (normally) gets installed at /bin
# Except that in the manylinux builds it's placed at /opt/python/[version]/bin/
# (as a symlink at least) which is *not* on the path.
# If cmake is a known module, import it and use it tell us its binary directory
if pkgutil.find_loader('cmake') is not None:
    import cmake
Simon Ward's avatar
Simon Ward committed
51

Simon Ward's avatar
Simon Ward committed
52
53
54
55
    CMAKE_BIN = cmake.CMAKE_BIN_DIR + os.path.sep + 'cmake'
else:
    CMAKE_BIN = 'cmake'

Simon Ward's avatar
Simon Ward committed
56

Simon Ward's avatar
Simon Ward committed
57
58
59
def get_cmake():
    return CMAKE_BIN

Simon Ward's avatar
Simon Ward committed
60

Simon Ward's avatar
Simon Ward committed
61
class CMakeExtension(Extension):
Simon Ward's avatar
Simon Ward committed
62
63
64
65
66
    """
    An extension to run the cmake build
    This simply overrides the base extension class so that setuptools
    doesn't try to build your sources for you
    """
Simon Ward's avatar
Simon Ward committed
67

Simon Ward's avatar
Simon Ward committed
68
    def __init__(self, name, sources=[]):
Simon Ward's avatar
Simon Ward committed
69
        super().__init__(name=name, sources=sources)
Simon Ward's avatar
Simon Ward committed
70

Simon Ward's avatar
Simon Ward committed
71
72
73
74
75
76
77
78

class InstallCMakeLibsData(install_data):
    """
    Just a wrapper to get the install data into the egg-info
    Listing the installed files in the egg-info guarantees that
    all of the package files will be uninstalled when the user
    uninstalls your package through pip
    """
Simon Ward's avatar
Simon Ward committed
79
80

    def run(self):
Simon Ward's avatar
Simon Ward committed
81
        """
Simon Ward's avatar
Simon Ward committed
82
        Out files are the libraries that were built using cmake
Simon Ward's avatar
Simon Ward committed
83
84
85
86
87
88
89
90
        """
        # There seems to be no other way to do this; I tried listing the
        # libraries during the execution of the InstallCMakeLibs.run() but
        # setuptools never tracked them, seems like setuptools wants to
        # track the libraries through package data more than anything...

        self.outfiles = self.distribution.data_files

Simon Ward's avatar
Simon Ward committed
91

92

Simon Ward's avatar
Simon Ward committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class InstallCMakeLibs(install_lib):
    """
    Get the libraries from the parent distribution, use those as the outfiles
    Skip building anything; everything is already built, forward libraries to
    the installation step
    """

    def run(self):
        """
        Copy libraries from the bin directory and place them as appropriate
        """

        self.announce("Moving library files", level=3)
        # We have already built the libraries in the previous build_ext step
        self.skip_build = True

        # Depending on the files that are generated from your cmake
        # build chain, you may need to change the below code, such that
        # your files are moved to the appropriate location when the installation
        # is run

Simon Ward's avatar
Simon Ward committed
114
        # bin_dir = self.distribution.bin_dir
Simon Ward's avatar
Simon Ward committed
115
116
117
        bin_dir = os.path.abspath(os.path.join(self.distribution.bin_dir, '..'))

        libs = [os.path.join(bin_dir, _dir) for _dir in
Simon Ward's avatar
Simon Ward committed
118
119
                os.listdir(bin_dir) if
                os.path.isdir(os.path.join(bin_dir, _dir))]
Simon Ward's avatar
Simon Ward committed
120
121
122
123
124
125
126
127
128
129
130

        for lib in libs:
            shutil.move(lib, os.path.join(self.build_dir,
                                          os.path.basename(lib)))

        # Move the lib to the correct location.
        bin_dir = self.build_dir
        pyd_path = [os.path.join(bin_dir, _pyd) for _pyd in
                    os.listdir(bin_dir) if
                    os.path.isfile(os.path.join(bin_dir, _pyd)) and
                    os.path.splitext(_pyd)[0].startswith(PACKAGE_NAME) and
131
                    os.path.splitext(_pyd)[1] in [".pyd", ".so"]][0]
Simon Ward's avatar
Simon Ward committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
        shutil.move(pyd_path, os.path.join(os.path.split(pyd_path)[0], 'CFML_api', os.path.split(pyd_path)[1]))

        # Mark the libs for installation, adding them to
        # distribution.data_files seems to ensure that setuptools' record
        # writer appends them to installed-files.txt in the package's egg-info
        #
        # Also tried adding the libraries to the distribution.libraries list,
        # but that never seemed to add them to the installed-files.txt in the
        # egg-info, and the online recommendation seems to be adding libraries
        # into eager_resources in the call to setup(), which I think puts them
        # in data_files anyways.
        #
        # What is the best way?

        # These are the additional installation files that should be
        # included in the package, but are resultant of the cmake build
        # step; depending on the files that are generated from your cmake
        # build chain, you may need to modify the below code

        self.distribution.data_files = [os.path.join(self.install_dir,
                                                     os.path.basename(lib))
                                        for lib in libs]
Simon Ward's avatar
Simon Ward committed
154

Simon Ward's avatar
Simon Ward committed
155
        # Must be forced to run after adding the libs to data_files
Simon Ward's avatar
Simon Ward committed
156

Simon Ward's avatar
Simon Ward committed
157
        self.distribution.run_command("install_data")
Simon Ward's avatar
Simon Ward committed
158

Simon Ward's avatar
Simon Ward committed
159
        super().run()
Simon Ward's avatar
Simon Ward committed
160

161
162
        pass

Simon Ward's avatar
Simon Ward committed
163

Simon Ward's avatar
Simon Ward committed
164
165
166
167
class InstallCMakeScripts(install_scripts):
    """
    Install the scripts in the build dir
    """
Simon Ward's avatar
Simon Ward committed
168

Simon Ward's avatar
Simon Ward committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
    def run(self):
        """
        Copy the required directory to the build directory and super().run()
        """

        self.announce("Moving scripts files", level=3)

        # Scripts were already built in a previous step

        self.skip_build = True

        bin_dir = self.distribution.bin_dir
        scripts_dirs = []

        # scripts_dirs = [os.path.join(bin_dir, _dir) for _dir in
        #                 os.listdir(bin_dir) if
        #                 os.path.isdir(os.path.join(bin_dir, _dir))]
        #
        # for scripts_dir in scripts_dirs:
        #
        #     shutil.move(scripts_dir,
        #                 os.path.join(self.build_dir,
        #                              os.path.basename(scripts_dir)))

        # Mark the scripts for installation, adding them to
        # distribution.scripts seems to ensure that the setuptools' record
        # writer appends them to installed-files.txt in the package's egg-info

        self.distribution.scripts = scripts_dirs

        super().run()

Simon Ward's avatar
Simon Ward committed
201

Simon Ward's avatar
Simon Ward committed
202
203
204
205
206
207
208
209
210
211
212
class BuildCMakeExt(build_ext):
    """
    Builds using cmake instead of the python setuptools implicit build
    """

    def run(self):
        """
        Perform build_cmake before doing the 'normal' stuff
        """

        for extension in self.extensions:
Simon Ward's avatar
Simon Ward committed
213
            self.build_cmake(extension)
214
215
        # super().run()
        pass
Simon Ward's avatar
Simon Ward committed
216
217
218
219
220

    def build_cmake(self, extension: Extension):
        """
        The steps required to build the extension
        """
Simon Ward's avatar
Simon Ward committed
221
        cfg = "Debug" if self.debug else "Release"
Simon Ward's avatar
Simon Ward committed
222
223
224
225
        self.announce("Preparing the build environment", level=3)

        build_dir = pathlib.Path(self.build_temp)

226
227
228
229
230
231
232
233
        # don't store the abi3 info in filename
        extension._file_name = extension._full_name

        ext_path = pathlib.Path(self.get_ext_fullpath(extension.name))

        ext_ext = os.path.splitext(ext_path)[1]
        filename = extension._file_name + ext_ext
        extension_path = os.path.join(ext_path.parent, filename)
Simon Ward's avatar
Simon Ward committed
234
235

        os.makedirs(build_dir, exist_ok=True)
236
        os.makedirs(ext_path.parent.absolute(), exist_ok=True)
Simon Ward's avatar
Simon Ward committed
237
238
239
240
241
242
243

        # Now that the necessary directories are created, build

        self.announce("Configuring cmake project", level=3)

        # Change your cmake arguments below as necessary
        # Below is just an example set of arguments for building Blender as a Python module
Simon Ward's avatar
Simon Ward committed
244
245

        cmake_args = [
Simon Ward's avatar
Simon Ward committed
246
247
            '-H' + SOURCE_DIR,
            '-B' + self.build_temp,
Simon Ward's avatar
Simon Ward committed
248
            "-DPYTHON_EXECUTABLE:FILEPATH={}".format(sys.executable),
Simon Ward's avatar
Simon Ward committed
249
            "-DARCH32=OFF",
Simon Ward's avatar
Simon Ward committed
250
            "-DCMAKE_Fortran_COMPILER={}".format(COMPILER),
Simon Ward's avatar
Simon Ward committed
251
252
253
            "-DPYTHON_API=ON",
            "-DUSE_HDF=OFF",
            "-DCMAKE_BUILD_TYPE={}".format(cfg),  # not used on MSVC, but no harm
Simon Ward's avatar
Simon Ward committed
254
            "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}".format(os.path.join(build_dir), 'Release'),
255
            "-DPYSETUP=ON"
Simon Ward's avatar
Simon Ward committed
256
257
258
259
        ]
        build_args = []

        check_call(
Simon Ward's avatar
Simon Ward committed
260
            [get_cmake()] + cmake_args
Simon Ward's avatar
Simon Ward committed
261
        )
Simon Ward's avatar
Simon Ward committed
262
263
264

        self.announce("Building binaries", level=3)

Simon Ward's avatar
Simon Ward committed
265
        check_call(
Simon Ward's avatar
Simon Ward committed
266
            [get_cmake(), "--build", ".", '--target', 'install'] + build_args, cwd=self.build_temp
Simon Ward's avatar
Simon Ward committed
267
        )
Simon Ward's avatar
Simon Ward committed
268

Simon Ward's avatar
Simon Ward committed
269
270
271
272
273
274
275
276
277
278
279
        # Build finished, now copy the files into the copy directory
        # The copy directory is the parent directory of the extension (.pyd)
        self.announce("Moving built python module", level=3)

        bin_dir = os.path.join(os.getcwd(), COMPILER, 'Python_API', 'CFML_api')
        self.distribution.bin_dir = bin_dir

        pyd_path = [os.path.join(bin_dir, _pyd) for _pyd in
                    os.listdir(bin_dir) if
                    os.path.isfile(os.path.join(bin_dir, _pyd)) and
                    os.path.splitext(_pyd)[0].startswith(PACKAGE_NAME) and
280
                    os.path.splitext(_pyd)[1] in [".pyd", ".so"]][0]
Simon Ward's avatar
Simon Ward committed
281
282
283
284
285
286
287
288
289
290
291

        shutil.move(pyd_path, extension_path)
        # After build_ext is run, the following commands will run:
        #
        # install_lib
        # install_scripts
        #
        # These commands are subclassed above to avoid pitfalls that
        # setuptools tries to impose when installing these, as it usually
        # wants to build those libs and scripts as well or move them to a
        # different place. See comments above for additional information
Simon Ward's avatar
Simon Ward committed
292

Simon Ward's avatar
Simon Ward committed
293

294
295
296
297
298
299
300
301
302
303
class BuildCommand(distutils.command.build.build):

    def initialize_options(self):
        # this overrides the directory names for
        # build/lib and build/temp
        distutils.command.build.build.initialize_options(self)
        self.build_platlib = 'build/lib'
        self.build_temp = 'build/temp'


Simon Ward's avatar
Simon Ward committed
304
305
306
307
setup(name="CFML",
      version="0.0.1",
      author="Simon Ward",
      author_email="simon.ward@ess.eu",
Simon Ward's avatar
Simon Ward committed
308
309
310
      description="The Crystallographic Fortran Modules Library (CrysFML) is a set of modules containing "
                  "procedures of interest in Crystallographic applications.",
      ext_modules=[CMakeExtension(name=PACKAGE_NAME)],
Simon Ward's avatar
Simon Ward committed
311
312
      long_description=open("./README", 'r').read(),
      long_description_content_type="text/markdown",
Simon Ward's avatar
Simon Ward committed
313
      keywords="crystallography, physics, neutron, diffraction",
Simon Ward's avatar
Simon Ward committed
314
315
316
317
      classifiers=["Intended Audience :: Developers",
                   "License :: OSI Approved :: "
                   "GNU Lesser General Public License v3 (LGPLv3)",
                   "Natural Language :: English",
Simon Ward's avatar
Simon Ward committed
318
                   "Programming Language :: Fortran",
Simon Ward's avatar
Simon Ward committed
319
                   "Programming Language :: Python"],
Simon Ward's avatar
Simon Ward committed
320
      license='LGPL',
321
      cpython_tags=None,
Simon Ward's avatar
Simon Ward committed
322
      cmdclass={
323
          'build':           BuildCommand,
Simon Ward's avatar
Simon Ward committed
324
325
326
          'build_ext':       BuildCMakeExt,
          'install_data':    InstallCMakeLibsData,
          'install_lib':     InstallCMakeLibs,
327
328
          'install_scripts': InstallCMakeScripts,
          'bdist_wheel':     Bdist_wheel
Simon Ward's avatar
Simon Ward committed
329
330
      },
      setup_requires=['wheel']
331
      )