Skip to content
Snippets Groups Projects
setup.py 11.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • import os
    import pathlib
    import pkgutil
    import shutil
    import struct
    import sys
    
    Simon Ward's avatar
    Simon Ward committed
    import setuptools
    
    Simon Ward's avatar
    Simon Ward committed
    import distutils.sysconfig as sysconfig
    
    Simon Ward's avatar
    Simon Ward committed
    from distutils.core import setup
    
    import distutils.command.build
    
    Simon Ward's avatar
    Simon Ward committed
    from distutils.command.install_data import install_data
    
    from subprocess import CalledProcessError, check_output, check_call
    
    
    Simon Ward's avatar
    Simon Ward committed
    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
    
    
    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
    BITS = struct.calcsize("P") * 8
    PACKAGE_NAME = "crysfml_api"
    SOURCE_DIR = '.'
    COMPILER = 'gfortran'
    
    Simon Ward's avatar
    Simon Ward committed
    if os.environ.get('FC', False):
        COMPILER = os.environ.get('FC')
    print(f'Compiler set to: {COMPILER}')
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
    # 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
        CMAKE_BIN = cmake.CMAKE_BIN_DIR + os.path.sep + 'cmake'
    else:
        CMAKE_BIN = 'cmake'
    
    
    Simon Ward's avatar
    Simon Ward committed
    def get_cmake():
        return CMAKE_BIN
    
    
    Simon Ward's avatar
    Simon Ward committed
    class CMakeExtension(Extension):
    
    Simon Ward's avatar
    Simon Ward committed
        """
        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
    
    
    Simon Ward's avatar
    Simon Ward committed
        def __init__(self, name, sources=[]):
    
            super().__init__(name=name, sources=sources)
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
    
    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
    
        def run(self):
    
    Simon Ward's avatar
    Simon Ward committed
            """
    
            Out files are the libraries that were built using cmake
    
    Simon Ward's avatar
    Simon Ward committed
            """
            # 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
    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
    
    
            # bin_dir = self.distribution.bin_dir
    
    Simon Ward's avatar
    Simon Ward committed
            bin_dir = os.path.abspath(os.path.join(self.distribution.bin_dir, '..'))
    
            libs = [os.path.join(bin_dir, _dir) for _dir in
    
                    os.listdir(bin_dir) if
                    os.path.isdir(os.path.join(bin_dir, _dir))]
    
    Simon Ward's avatar
    Simon Ward committed
    
            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
    
                        os.path.splitext(_pyd)[1] in [".pyd", ".so"]][0]
    
    Simon Ward's avatar
    Simon Ward committed
            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
    
    
    Simon Ward's avatar
    Simon Ward committed
            # Must be forced to run after adding the libs to data_files
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
            self.distribution.run_command("install_data")
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
            super().run()
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
    class InstallCMakeScripts(install_scripts):
        """
        Install the scripts in the build dir
        """
    
    Simon Ward's avatar
    Simon Ward committed
    
    
    Simon Ward's avatar
    Simon Ward committed
        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
    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:
    
                self.build_cmake(extension)
    
            # super().run()
            pass
    
    Simon Ward's avatar
    Simon Ward committed
    
        def build_cmake(self, extension: Extension):
            """
            The steps required to build the extension
            """
    
    Simon Ward's avatar
    Simon Ward committed
            cfg = "Debug" if self.debug else "Release"
    
    Simon Ward's avatar
    Simon Ward committed
            self.announce("Preparing the build environment", level=3)
    
            build_dir = pathlib.Path(self.build_temp)
    
    
            # 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
    
            os.makedirs(build_dir, exist_ok=True)
    
            os.makedirs(ext_path.parent.absolute(), exist_ok=True)
    
    Simon Ward's avatar
    Simon Ward committed
    
            # 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
    
            cmake_args = [
    
    Simon Ward's avatar
    Simon Ward committed
                '-H' + SOURCE_DIR,
                '-B' + self.build_temp,
    
    Simon Ward's avatar
    Simon Ward committed
                "-DPYTHON_EXECUTABLE:FILEPATH={}".format(sys.executable),
    
    Simon Ward's avatar
    Simon Ward committed
                "-DARCH32=OFF",
    
    Simon Ward's avatar
    Simon Ward committed
                "-DCMAKE_Fortran_COMPILER={}".format(COMPILER),
    
    Simon Ward's avatar
    Simon Ward committed
                "-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
                "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}".format(os.path.join(build_dir), 'Release'),
    
                "-DPYSETUP=ON"
    
    Simon Ward's avatar
    Simon Ward committed
            ]
            build_args = []
    
            check_call(
    
    Simon Ward's avatar
    Simon Ward committed
                [get_cmake()] + cmake_args
    
    Simon Ward's avatar
    Simon Ward committed
            )
    
    Simon Ward's avatar
    Simon Ward committed
    
            self.announce("Building binaries", level=3)
    
    
    Simon Ward's avatar
    Simon Ward committed
            check_call(
    
    Simon Ward's avatar
    Simon Ward committed
                [get_cmake(), "--build", ".", '--target', 'install'] + build_args, cwd=self.build_temp
    
    Simon Ward's avatar
    Simon Ward committed
            )
    
    Simon Ward's avatar
    Simon Ward committed
            # 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
    
                        os.path.splitext(_pyd)[1] in [".pyd", ".so"]][0]
    
    Simon Ward's avatar
    Simon Ward committed
    
            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
    
    
    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
    setup(name="CFML",
          version="0.0.1",
          author="Simon Ward",
          author_email="simon.ward@ess.eu",
    
          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
          long_description=open("./README", 'r').read(),
          long_description_content_type="text/markdown",
    
          keywords="crystallography, physics, neutron, diffraction",
    
    Simon Ward's avatar
    Simon Ward committed
          classifiers=["Intended Audience :: Developers",
                       "License :: OSI Approved :: "
                       "GNU Lesser General Public License v3 (LGPLv3)",
                       "Natural Language :: English",
    
                       "Programming Language :: Fortran",
    
    Simon Ward's avatar
    Simon Ward committed
                       "Programming Language :: Python"],
    
          license='LGPL',
    
          cpython_tags=None,
    
    Simon Ward's avatar
    Simon Ward committed
          cmdclass={
    
              'build':           BuildCommand,
    
              'build_ext':       BuildCMakeExt,
              'install_data':    InstallCMakeLibsData,
              'install_lib':     InstallCMakeLibs,
    
              'install_scripts': InstallCMakeScripts,
              'bdist_wheel':     Bdist_wheel
    
    Simon Ward's avatar
    Simon Ward committed
          },
          setup_requires=['wheel']