The ${CMAKE_CURRENT_BINARY_DIR} directory contains the compiled account.cpython-36m-x86_64-linux-gnu.so Python module built using pybind11, but note that its name depends on the operating system (in this case, 64-bit Linux) and the Python environment (in this case, Python 3.6). The setup.py script will run CMake under the hood and install the Python module into the correct path, depending on the selected Python environment (system Python or Pipenv or Virtual Environment). But now we have two challenges when installing the module:
- The naming can change.
- The path is set outside of CMake.
We can solve this by using the following install target, where setup.py will define the install target location:
install(
TARGETS
account
LIBRARY
DESTINATION account
)
Here we instruct CMake to install the compiled Python module file into the account subdirectory relative to the install target location (Chapter 10, Writing an Installer, discusses in detail how the target location can be set). The latter will be set by setup.py by defining CMAKE_INSTALL_PREFIX to point to the right path depending on the Python environment.
Let us now inspect how we achieve this in setup.py; we will start from the bottom of the script:
setup(
name=_this_package,
version=version['__version__'],
description='Description in here.',
long_description=long_description,
author='Bruce Wayne',
author_email='bruce.wayne@example.com',
url='http://example.com',
license='MIT',
packages=[_this_package],
include_package_data=True,
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Science/Research',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.6'
],
cmdclass={'build': extend_build()})
The script contains a number of placeholders and hopefully self-explaining directives, but here we will focus on the last directive, cmdclass, where we extend the default build step by a custom function, which we call extend_build. This function subclasses the default build step:
def extend_build():
class build(_build.build):
def run(self):
cwd = os.getcwd()
if spawn.find_executable('cmake') is None:
sys.stderr.write("CMake is required to build this package.\n")
sys.exit(-1)
_source_dir = os.path.split(__file__)[0]
_build_dir = os.path.join(_source_dir, 'build_setup_py')
_prefix = get_python_lib()
try:
cmake_configure_command = [
'cmake',
'-H{0}'.format(_source_dir),
'-B{0}'.format(_build_dir),
'-DCMAKE_INSTALL_PREFIX={0}'.format(_prefix),
]
_generator = os.getenv('CMAKE_GENERATOR')
if _generator is not None:
cmake_configure_command.append('-G{0}'.format(_generator))
spawn.spawn(cmake_configure_command)
spawn.spawn(
['cmake', '--build', _build_dir, '--target', 'install'])
os.chdir(cwd)
except spawn.DistutilsExecError:
sys.stderr.write("Error while building with CMake\n")
sys.exit(-1)
_build.build.run(self)
return build
First, the function checks whether CMake is available on the system. The core of the function executes two CMake commands:
cmake_configure_command = [
'cmake',
'-H{0}'.format(_source_dir),
'-B{0}'.format(_build_dir),
'-DCMAKE_INSTALL_PREFIX={0}'.format(_prefix),
]
_generator = os.getenv('CMAKE_GENERATOR')
if _generator is not None:
cmake_configure_command.append('-G{0}'.format(_generator))
spawn.spawn(cmake_configure_command)
spawn.spawn(
['cmake', '--build', _build_dir, '--target', 'install'])
Here we have the possibility to change the default generator used by setting the CMAKE_GENERATOR environment variable. The install prefix is defined as follows:
_prefix = get_python_lib()
The get_python_lib function imported from distutils.sysconfig provides the root directory for the install prefix. The cmake --build _build_dir --target install command builds and installs our project in one step in a portable way. The reason why we use the name _build_dir instead of simply build is that your project might already contain a build directory when testing the local install, which would conflict with a fresh installation. For packages already uploaded to PyPI, the name of the build directory does not make a difference.