#.rst # Define a function to create Cython modules. # # For more information on the Cython project, see http://cython.org/. # "Cython is a language that makes writing C extensions for the Python language # as easy as Python itself." # # This file defines a CMake function to build a Cython Python module. # To use it, first include this file. # # include(UseCython) # # The following functions are defined: # # add_cython_target( [] # [EMBED_MAIN] # [C | CXX] # [PY2 | PY3] # [OUTPUT_VAR ]) # # Create a custom rule to generate the source code for a Python extension module # using cython. ```` is the name of the new target, and ```` # is the path to a cython source file. Note that, despite the name, no new # targets are created by this function. Instead, see ``OUTPUT_VAR`` for # retrieving the path to the generated source for subsequent targets. # # If only ```` is provided, and it ends in the ".pyx" extension, then it # is assumed to be the ````. The name of the input without the # extension is used as the target name. If only ```` is provided, and it # does not end in the ".pyx" extension, then the ```` is assumed to # be ``.pyx``. # # The Cython include search path is amended with any entries found in the # ``INCLUDE_DIRECTORIES`` property of the directory containing the # ```` file. Use ``iunclude_directories`` to add to the Cython # include search path. # # Options: # # ``EMBED_MAIN`` # Embed a main() function in the generated output (for stand-alone # applications that initialize their own Python runtime). # # ``C | CXX`` # Force the generation of either a C or C++ file. By default, a C file is # generated, unless the C language is not enabled for the project; in this # case, a C++ file is generated by default. # # ``PY2 | PY3`` # Force compilation using either Python-2 or Python-3 syntax and code # semantics. By default, Python-2 syntax and semantics are used if the major # version of Python found is 2. Otherwise, Python-3 syntax and sematics are # used. # # ``OUTPUT_VAR `` # Set the variable ```` in the parent scope to the path to the # generated source file. By default, ```` is used as the output # variable name. # # Defined variables: # # ```` # The path of the generated source file. # # # Example usage: # # .. code-block:: cmake # # find_package(Cython) # # # Note: In this case, either one of these arguments may be omitted; their # # value would have been inferred from that of the other. # add_cython_target(cy_code cy_code.pyx) # # add_library(cy_code MODULE ${cy_code}) # target_link_libraries(cy_code ...) # # Cache variables that effect the behavior include: # # ``CYTHON_ANNOTATE`` # whether to create an annotated .html file when compiling # # ``CYTHON_FLAGS`` # additional flags to pass to the Cython compiler # # See also FindCython.cmake # #============================================================================= # Copyright 2011 Kitware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #============================================================================= # Configuration options. set(CYTHON_ANNOTATE OFF CACHE BOOL "Create an annotated .html file when compiling *.pyx.") set(CYTHON_FLAGS "" CACHE STRING "Extra flags to the cython compiler.") mark_as_advanced(CYTHON_ANNOTATE CYTHON_FLAGS) find_package(PythonLibs REQUIRED) set(CYTHON_CXX_EXTENSION "cxx") set(CYTHON_C_EXTENSION "c") get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES) function(add_cython_target _name) set(options EMBED_MAIN C CXX PY2 PY3) set(options1 OUTPUT_VAR) cmake_parse_arguments(_args "${options}" "${options1}" "" ${ARGN}) list(GET _args_UNPARSED_ARGUMENTS 0 _arg0) # if provided, use _arg0 as the input file path if(_arg0) set(_source_file ${_arg0}) # otherwise, must determine source file from name, or vice versa else() get_filename_component(_name_ext "${_name}" EXT) # if extension provided, _name is the source file if(_name_ext) set(_source_file ${_name}) get_filename_component(_name "${_source_file}" NAME_WE) # otherwise, assume the source file is ${_name}.pyx else() set(_source_file ${_name}.pyx) endif() endif() set(_embed_main FALSE) if("C" IN_LIST languages) set(_output_syntax "C") elseif("CXX" IN_LIST languages) set(_output_syntax "CXX") else() message(FATAL_ERROR "Either C or CXX must be enabled to use Cython") endif() if("${PYTHONLIBS_VERSION_STRING}" MATCHES "^2.") set(_input_syntax "PY2") else() set(_input_syntax "PY3") endif() if(_args_EMBED_MAIN) set(_embed_main TRUE) endif() if(_args_C) set(_output_syntax "C") endif() if(_args_CXX) set(_output_syntax "CXX") endif() if(_args_PY2) set(_input_syntax "PY2") endif() if(_args_PY3) set(_input_syntax "PY3") endif() set(embed_arg "") if(_embed_main) set(embed_arg "--embed") endif() set(cxx_arg "") set(extension "c") if(_output_syntax STREQUAL "CXX") set(cxx_arg "--cplus") set(extension "cxx") endif() set(py_version_arg "") if(_input_syntax STREQUAL "PY2") set(py_version_arg "-2") elseif(_input_syntax STREQUAL "PY3") set(py_version_arg "-3") endif() set(generated_file "${CMAKE_CURRENT_BINARY_DIR}/${_name}.${extension}") set_source_files_properties(${generated_file} PROPERTIES GENERATED TRUE) set(_output_var ${_name}) if(_args_OUTPUT_VAR) set(_output_var ${_args_OUTPUT_VAR}) endif() set(${_output_var} ${generated_file} PARENT_SCOPE) file(RELATIVE_PATH generated_file_relative ${CMAKE_BINARY_DIR} ${generated_file}) set(comment "Generating ${_output_syntax} source ${generated_file_relative}") set(cython_include_directories "") set(pxd_dependencies "") set(c_header_dependencies "") # Get the include directories. get_source_file_property(pyx_location ${_source_file} LOCATION) get_filename_component(pyx_path ${pyx_location} PATH) get_directory_property(cmake_include_directories DIRECTORY ${pyx_path} INCLUDE_DIRECTORIES) list(APPEND cython_include_directories ${cmake_include_directories}) # Determine dependencies. # Add the pxd file with the same basename as the given pyx file. get_filename_component(pyx_file_basename ${_source_file} NAME_WE) unset(corresponding_pxd_file CACHE) find_file(corresponding_pxd_file ${pyx_file_basename}.pxd PATHS "${pyx_path}" ${cmake_include_directories} NO_DEFAULT_PATH) if(corresponding_pxd_file) list(APPEND pxd_dependencies "${corresponding_pxd_file}") endif() # pxd files to check for additional dependencies set(pxds_to_check "${_source_file}" "${pxd_dependencies}") set(pxds_checked "") set(number_pxds_to_check 1) while(number_pxds_to_check GREATER 0) foreach(pxd ${pxds_to_check}) list(APPEND pxds_checked "${pxd}") list(REMOVE_ITEM pxds_to_check "${pxd}") # look for C headers file(STRINGS "${pxd}" extern_from_statements REGEX "cdef[ ]+extern[ ]+from.*$") foreach(statement ${extern_from_statements}) # Had trouble getting the quote in the regex string(REGEX REPLACE "cdef[ ]+extern[ ]+from[ ]+[\"]([^\"]+)[\"].*" "\\1" header "${statement}") unset(header_location CACHE) find_file(header_location ${header} PATHS ${cmake_include_directories}) if(header_location) list(FIND c_header_dependencies "${header_location}" header_idx) if(${header_idx} LESS 0) list(APPEND c_header_dependencies "${header_location}") endif() endif() endforeach() # check for pxd dependencies # Look for cimport statements. set(module_dependencies "") file(STRINGS "${pxd}" cimport_statements REGEX cimport) foreach(statement ${cimport_statements}) if(${statement} MATCHES from) string(REGEX REPLACE "from[ ]+([^ ]+).*" "\\1" module "${statement}") else() string(REGEX REPLACE "cimport[ ]+([^ ]+).*" "\\1" module "${statement}") endif() list(APPEND module_dependencies ${module}) endforeach() # check for pxi dependencies # Look for include statements. set(include_dependencies "") file(STRINGS "${pxd}" include_statements REGEX include) foreach(statement ${include_statements}) string(REGEX REPLACE "include[ ]+[\"]([^\"]+)[\"].*" "\\1" module "${statement}") list(APPEND include_dependencies ${module}) endforeach() list(REMOVE_DUPLICATES module_dependencies) list(REMOVE_DUPLICATES include_dependencies) # Add modules to the files to check, if appropriate. foreach(module ${module_dependencies}) unset(pxd_location CACHE) find_file(pxd_location ${module}.pxd PATHS "${pyx_path}" ${cmake_include_directories} NO_DEFAULT_PATH) if(pxd_location) list(FIND pxds_checked ${pxd_location} pxd_idx) if(${pxd_idx} LESS 0) list(FIND pxds_to_check ${pxd_location} pxd_idx) if(${pxd_idx} LESS 0) list(APPEND pxds_to_check ${pxd_location}) list(APPEND pxd_dependencies ${pxd_location}) endif() # if it is not already going to be checked endif() # if it has not already been checked endif() # if pxd file can be found endforeach() # for each module dependency discovered # Add includes to the files to check, if appropriate. foreach(_include ${include_dependencies}) unset(pxi_location CACHE) find_file(pxi_location ${_include} PATHS "${pyx_path}" ${cmake_include_directories} NO_DEFAULT_PATH) if(pxi_location) list(FIND pxds_checked ${pxi_location} pxd_idx) if(${pxd_idx} LESS 0) list(FIND pxds_to_check ${pxi_location} pxd_idx) if(${pxd_idx} LESS 0) list(APPEND pxds_to_check ${pxi_location}) list(APPEND pxd_dependencies ${pxi_location}) endif() # if it is not already going to be checked endif() # if it has not already been checked endif() # if include file can be found endforeach() # for each include dependency discovered endforeach() # for each include file to check list(LENGTH pxds_to_check number_pxds_to_check) endwhile() # Set additional flags. set(annotate_arg "") if(CYTHON_ANNOTATE) set(annotate_arg "--annotate") endif() set(no_docstrings_arg "") if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") set(no_docstrings_arg "--no-docstrings") endif() set(cython_debug_arg "") set(embed_pos_arg "") set(line_directives_arg "") if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") set(cython_debug_arg "--gdb") set(embed_pos_arg "--embed-positions") set(line_directives_arg "--line-directives") endif() # Include directory arguments. list(REMOVE_DUPLICATES cython_include_directories) set(include_directory_arg "") foreach(_include_dir ${cython_include_directories}) set(include_directory_arg ${include_directory_arg} "--include-dir" "${_include_dir}") endforeach() list(REMOVE_DUPLICATES pxd_dependencies) list(REMOVE_DUPLICATES c_header_dependencies) # Add the command to run the compiler. add_custom_command(OUTPUT ${generated_file} COMMAND ${CYTHON_EXECUTABLE} ARGS ${cxx_arg} ${include_directory_arg} ${py_version_arg} ${embed_arg} ${annotate_arg} # We always want to generate docstrings # ${no_docstrings_arg} ${cython_debug_arg} ${embed_pos_arg} ${line_directives_arg} ${CYTHON_FLAGS} ${pyx_location} --output-file ${generated_file} DEPENDS ${_source_file} ${pxd_dependencies} IMPLICIT_DEPENDS ${_output_syntax} ${c_header_dependencies} COMMENT ${comment}) # NOTE(opadron): I thought about making a proper target, but after trying it # out, I decided that it would be far too convenient to use the same name as # the target for the extension module (e.g.: for single-file modules): # # ... # add_cython_target(_module.pyx) # add_library(_module ${_module}) # ... # # The above example would not be possible since the "_module" target name # would already be taken by the cython target. Since I can't think of a # reason why someone would need the custom target instead of just using the # generated file directly, I decided to leave this commented out. # # add_custom_target(${_name} DEPENDS ${generated_file}) # Remove their visibility to the user. set(corresponding_pxd_file "" CACHE INTERNAL "") set(header_location "" CACHE INTERNAL "") set(pxd_location "" CACHE INTERNAL "") endfunction()