# Filename: Python.cmake # # Description: This file provides support functions for building/installing # Python extension modules and/or pure-Python packages. # # Functions: # add_python_target(target [source1 [source2 ...]]) # install_python_package(path [ARCH/LIB]) # ensure_python_init(path [ARCH] [ROOT] [OVERWRITE]) # # # Function: add_python_target(target [EXPORT exp] [COMPONENT comp] # [source1 [source2 ...]]) # Build the provided source(s) as a Python extension module, linked against the # Python runtime library. # # Note that this also takes care of installation, unlike other target creation # commands in CMake. The EXPORT and COMPONENT keywords allow passing the # corresponding options to install(), but default to "Python" otherwise. # function(add_python_target target) if(NOT HAVE_PYTHON) return() endif() string(REGEX REPLACE "^.*\\." "" basename "${target}") set(sources) set(component "Python") set(export "Python") foreach(arg ${ARGN}) if(arg STREQUAL "COMPONENT") set(keyword "component") elseif(arg STREQUAL "EXPORT") set(keyword "export") elseif(keyword) set(${keyword} "${arg}") unset(keyword) else() list(APPEND sources "${arg}") endif() endforeach(arg) string(REGEX REPLACE "\\.[^.]+$" "" namespace "${target}") string(REPLACE "." "/" slash_namespace "${namespace}") add_library(${target} ${MODULE_TYPE} ${sources}) target_link_libraries(${target} PKG::PYTHON) if(BUILD_SHARED_LIBS) set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${slash_namespace}" OUTPUT_NAME "${basename}" PREFIX "" SUFFIX "${PYTHON_EXTENSION_SUFFIX}") if(PYTHON_ARCH_INSTALL_DIR) install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION "${PYTHON_ARCH_INSTALL_DIR}/${slash_namespace}") endif() else() set_target_properties(${target} PROPERTIES OUTPUT_NAME "${basename}" PREFIX "libpy.${namespace}.") install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION lib) endif() set(keywords OVERWRITE ARCH) if(NOT slash_namespace MATCHES ".*/.*") list(APPEND keywords ROOT) endif() ensure_python_init("${PROJECT_BINARY_DIR}/${slash_namespace}" ${keywords}) endfunction(add_python_target) # # Function: install_python_package(name [SOURCE path] [ARCH/LIB] [COMPONENT component]) # # Installs the Python package `name` (which may have its source at `path`). # # The package is copied to (or created in) the build directory so that the user # may import it before the install step. # # Note that this handles more than just installation; it will also invoke # Python's compileall utility to pregenerate .pyc/.pyo files. This will only # happen if the Python interpreter is found. # # The ARCH or LIB keyword may be used to specify whether this package should be # installed into Python's architecture-dependent or architecture-independent # package path. The default, if unspecified, is LIB. # # The COMPONENT keyword overrides the install component (see CMake's # documentation for more information on what this does). The default is # "Python". # function(install_python_package package_name) set(type "LIB") unset(keyword) set(component "Python") unset(src_path) foreach(arg ${ARGN}) if(arg STREQUAL "ARCH") set(type "ARCH") elseif(arg STREQUAL "LIB") set(type "LIB") elseif(arg STREQUAL "COMPONENT") set(keyword "${arg}") elseif(keyword STREQUAL "COMPONENT") set(component "${arg}") unset(keyword) elseif(arg STREQUAL "SOURCE") set(keyword "${arg}") elseif(keyword STREQUAL "SOURCE") set(src_path "${arg}") unset(keyword) else() message(FATAL_ERROR "install_python_package got unexpected argument: ${ARGN}") endif() endforeach(arg) set(path "${PROJECT_BINARY_DIR}/${package_name}") set(args -D "OUTPUT_DIR=${path}") if(src_path) list(APPEND args -D "SOURCE_DIR=${src_path}") endif() if(PYTHON_EXECUTABLE) list(APPEND args -D "PYTHON_EXECUTABLES=${PYTHON_EXECUTABLE}") endif() add_custom_target(${package_name} ALL COMMAND ${CMAKE_COMMAND} ${args} -P "${CMAKE_SOURCE_DIR}/cmake/scripts/CopyPython.cmake") set(dir "${PYTHON_${type}_INSTALL_DIR}") if(dir) install(DIRECTORY "${path}" DESTINATION "${dir}" COMPONENT "${component}" FILES_MATCHING REGEX "\\.py[co]?$") endif() endfunction(install_python_package) # # Function: ensure_python_init(path [ARCH] [ROOT] [OVERWRITE]) # # Makes sure that the directory - at `path` - contains a file named # '__init__.py', which is necessary for Python to recognize the directory as a # package. # # ARCH, if specified, means that this is a binary package, and the build tree # might contain configuration-specific subdirectories. The __init__.py will be # generated with a function that ensures that the appropriate configuration # subdirectory is in the path. # # ROOT, if specified, means that the directory may sit directly adjacent to a # 'bin' directory, which should be added to the DLL search path on Windows. # # OVERWRITE causes the __init__.py file to be overwritten if one is already # present. # function(ensure_python_init path) set(arch OFF) set(root OFF) set(overwrite OFF) foreach(arg ${ARGN}) if(arg STREQUAL "ARCH") set(arch ON) elseif(arg STREQUAL "ROOT") set(root ON) elseif(arg STREQUAL "OVERWRITE") set(overwrite ON) else() message(FATAL_ERROR "ensure_python_init got unexpected argument: ${arg}") endif() endforeach(arg) set(init_filename "${path}/__init__.py") if(EXISTS "${init_filename}" AND NOT overwrite) return() endif() file(WRITE "${init_filename}" "") if(arch AND NOT "${CMAKE_CFG_INTDIR}" STREQUAL ".") # ARCH set, and this is a multi-configuration generator set(configs "${CMAKE_CONFIGURATION_TYPES}") # Debug should be at the end (highest preference) list(REMOVE_ITEM configs "Debug") list(APPEND configs "Debug") string(REPLACE ";" "', '" configs "${configs}") file(APPEND "${init_filename}" " def _fixup_path(): try: path = __path__[0] except (NameError, IndexError): return # Not a package, or not on filesystem import os abspath = os.path.abspath(path) newpath = None for config in ['${configs}']: cfgpath = os.path.join(abspath, config) if not os.path.isdir(cfgpath): continue newpath = cfgpath if config.lower() == os.environ.get('CMAKE_CONFIGURATION', '').lower(): break if newpath: __path__.insert(0, newpath) _fixup_path() del _fixup_path ") endif() if(root AND WIN32 AND NOT CYGWIN) # ROOT set, and this is Windows file(APPEND "${init_filename}" " def _fixup_dlls(): try: path = __path__[0] except (NameError, IndexError): return # Not a package, or not on filesystem import os relpath = os.path.relpath(path, __path__[-1]) dll_path = os.path.abspath(os.path.join(__path__[-1], '../bin', relpath)) if not os.path.isdir(dll_path): return os_path = os.environ.get('PATH', '') os_path = os_path.split(os.pathsep) if os_path else [] os_path.insert(0, dll_path) os.environ['PATH'] = os.pathsep.join(os_path) _fixup_dlls() del _fixup_dlls ") endif() endfunction(ensure_python_init)