Skip to content

Commit 0cfee10

Browse files
committed
feat: rewrite
Signed-off-by: Henry Schreiner <[email protected]> feat: deduce C/CXX if possible Signed-off-by: Henry Schreiner <[email protected]> fix: better handling of output Signed-off-by: Henry Schreiner <[email protected]> docs: add info Signed-off-by: Henry Schreiner <[email protected]>
1 parent e89b851 commit 0cfee10

File tree

2 files changed

+152
-122
lines changed

2 files changed

+152
-122
lines changed

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,74 @@
1515

1616
<!-- SPHINX-START -->
1717

18+
19+
This provides helpers for using Cython. Use:
20+
21+
```cmake
22+
find_package(Cython MODULE REQUIRED VERSION 3.0)
23+
include(UseCython)
24+
```
25+
26+
If you find Python beforehand, the search will take this into account. You can
27+
specify a version range on CMake 3.19+. This will define a `Cython::Cython`
28+
target (along with a matching `CYTHON_EXECUTABLE` variable). It will also
29+
provide the following helper function:
30+
31+
```cmake
32+
cython_transpile(<pyx_file>
33+
[LANGUAGE C | CXX]
34+
[CYTHON_ARGS <args> ...]
35+
[OUTPUT <OutputFile>]
36+
[OUTPUT_VARIABLE <OutputVariable>]
37+
)
38+
```
39+
40+
This function takes a pyx file and makes a matching `.c` / `.cxx` file in the
41+
current binary directory (exact path can be specified with `OUTPUT`). The
42+
location of the produced file is placed in the variable specified by
43+
`OUTPUT_VARIABLE` if given. Extra arguments to the Cython executable can be
44+
given with `CYTHON_ARGS`, and if this is not set, it will take a default from a
45+
`CYTHON_ARGS` variable.
46+
47+
If the `LANGUAGE` is not given, and both `C` and `CXX` are enabled globally,
48+
then the language will try to be deduced from a `# distutils: language=...`
49+
comment in the source file, and C will be used if not found.
50+
51+
This utility relies on the `DEPFILE` feature introduced for Ninja in CMake 3.7,
52+
and added for Make in CMake 3.20, and Visual Studio & Xcode in CMake 3.21.
53+
54+
## Example
55+
56+
```cmake
57+
find_package(
58+
Python
59+
COMPONENTS Interpreter Development.Module
60+
REQUIRED)
61+
find_package(Cython MODULE REQUIRED)
62+
63+
cython_transpile(simple.pyx LANGUAGE C OUTPUT_VARIABLE simple_c)
64+
65+
python_add_library(simple MODULE "${simple_c}" WITH_SOABI)
66+
```
67+
68+
## scikit-build-core
69+
70+
To use this package with scikit-build-core, you need to include it in your build
71+
requirements:
72+
73+
```toml
74+
[build-system]
75+
requires = ["scikit-build-core", "cython", "cython-cmake"]
76+
build-backend = "scikit_build_core.build"
77+
```
78+
79+
It is also recommended to require CMake 3.21:
80+
81+
```toml
82+
[tool.scikit-build]
83+
cmake.version = ">=3.21"
84+
```
85+
1886
## Vendoring
1987

2088
You can vendor FindCython and/or UseCython into your package, as well. This

src/cython_cmake/cmake/UseCython.cmake

Lines changed: 84 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
# )
5050
#
5151
# Python_add_library(_hello
52-
# MODULE ${_hello_source_files}
52+
# MODULE "${_hello_source_file}"
5353
# WITH_SOABI
5454
# )
5555
#
@@ -79,16 +79,19 @@ if(CMAKE_VERSION VERSION_LESS "3.8")
7979
endif()
8080

8181
function(Cython_transpile)
82-
set(_options )
83-
set(_one_value LANGUAGE OUTPUT OUTPUT_VARIABLE)
84-
set(_multi_value CYTHON_ARGS)
85-
86-
cmake_parse_arguments(_args
87-
"${_options}"
88-
"${_one_value}"
89-
"${_multi_value}"
90-
${ARGN}
82+
cmake_parse_arguments(
83+
PARSE_ARGV 0
84+
CYTHON
85+
""
86+
"OUTPUT;LANGUAGE;OUTPUT_VARIABLE"
87+
"CYTHON_ARGS"
9188
)
89+
set(ALL_INPUT ${CYTHON_UNPARSED_ARGUMENTS})
90+
list(LENGTH ALL_INPUT INPUT_LENGTH)
91+
if(NOT INPUT_LENGTH EQUAL 1)
92+
message(FATAL_ERROR "One and only one input file must be specified, got '${ALL_INPUT}'")
93+
endif()
94+
list(GET ALL_INPUT 0 INPUT)
9295

9396
if(DEFINED CYTHON_EXECUTABLE)
9497
set(_cython_command "${CYTHON_EXECUTABLE}")
@@ -101,142 +104,101 @@ function(Cython_transpile)
101104
endif()
102105

103106
# Default to CYTHON_ARGS if argument not specified
104-
if(NOT _args_CYTHON_ARGS AND DEFINED CYTHON_ARGS)
105-
set(_args_CYTHON_ARGS "${CYTHON_ARGS}")
106-
endif()
107-
108-
# Get input
109-
set(_source_files ${_args_UNPARSED_ARGUMENTS})
110-
list(LENGTH _source_files input_length)
111-
if(NOT input_length EQUAL 1)
112-
message(FATAL_ERROR "One and only one input file must be specified, got '${_source_files}'")
107+
if(NOT CYTHON_CYTHON_ARGS AND DEFINED CYTHON_ARGS)
108+
set(CYTHON_CYTHON_ARGS "${CYTHON_ARGS}")
113109
endif()
114110

115-
function(_transpile _source_file generated_file language)
116-
117-
if(language STREQUAL "C")
118-
set(_language_arg "")
119-
elseif(language STREQUAL "CXX")
120-
set(_language_arg "--cplus")
121-
else()
122-
message(FATAL_ERROR "_transpile language must be one of C or CXX")
123-
endif()
124-
125-
set_source_files_properties(${generated_file} PROPERTIES GENERATED TRUE)
126-
127-
# Generated depfile is expected to have the ".dep" extension and be located along
128-
# side the generated source file.
129-
set(_depfile ${generated_file}.dep)
130-
set(_depfile_arg "-M")
131-
132-
# Normalize the input path
133-
get_filename_component(_source_file "${_source_file}" ABSOLUTE)
134-
135-
# Pretty-printed output names
136-
file(RELATIVE_PATH generated_file_relative
137-
${CMAKE_BINARY_DIR} ${generated_file})
138-
file(RELATIVE_PATH source_file_relative
139-
${CMAKE_SOURCE_DIR} ${_source_file})
140-
set(comment "Generating ${_language} source '${generated_file_relative}' from '${source_file_relative}'")
141-
142-
# Get output directory to ensure its exists
143-
get_filename_component(output_directory "${generated_file}" DIRECTORY)
144-
145-
get_source_file_property(pyx_location ${_source_file} LOCATION)
146-
147-
# Add the command to run the compiler.
148-
add_custom_command(
149-
OUTPUT ${generated_file}
150-
COMMAND
151-
${CMAKE_COMMAND} -E make_directory ${output_directory}
152-
COMMAND
153-
${_cython_command}
154-
${_language_arg}
155-
"${_args_CYTHON_ARGS}"
156-
${_depfile_arg}
157-
${pyx_location}
158-
--output-file ${generated_file}
159-
COMMAND_EXPAND_LISTS
160-
MAIN_DEPENDENCY
161-
${_source_file}
162-
DEPFILE
163-
${_depfile}
164-
VERBATIM
165-
COMMENT ${comment}
166-
)
167-
endfunction()
168-
169-
function(_set_output _input_file _language _output_var)
170-
if(_language STREQUAL "C")
171-
set(_language_extension "c")
172-
elseif(_language STREQUAL "CXX")
173-
set(_language_extension "cxx")
174-
else()
175-
message(FATAL_ERROR "_set_output language must be one of C or CXX")
176-
endif()
177-
178-
# Can use cmake_path for CMake 3.20+
179-
# cmake_path(GET _input_file STEM basename)
180-
get_filename_component(_basename "${_input_file}" NAME_WE)
181-
182-
if(IS_ABSOLUTE ${_input_file})
183-
file(RELATIVE_PATH _input_relative ${CMAKE_CURRENT_SOURCE_DIR} ${_input_file})
184-
else()
185-
set(_input_relative ${_input_file})
186-
endif()
187-
188-
get_filename_component(_output_relative_dir "${_input_relative}" DIRECTORY)
189-
string(REPLACE "." "_" _output_relative_dir "${_output_relative_dir}")
190-
if(_output_relative_dir)
191-
set(_output_relative_dir "${_output_relative_dir}/")
192-
endif()
193-
194-
set(${_output_var} "${CMAKE_CURRENT_BINARY_DIR}/${_output_relative_dir}${_basename}.${_language_extension}" PARENT_SCOPE)
195-
endfunction()
196-
197-
set(generated_files)
198-
199-
list(GET _source_files 0 _source_file)
200-
201111
# Set target language
202-
set(_language ${_args_LANGUAGE})
203-
if(NOT _language)
112+
if(NOT CYTHON_LANGUAGE)
204113
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
114+
205115
if("C" IN_LIST _languages AND "CXX" IN_LIST _languages)
206116
# Try to compute language. Returns falsy if not found.
207-
_cython_compute_language(_language ${_source_file})
117+
_cython_compute_language(CYTHON_LANGUAGE ${INPUT})
118+
message(STATUS "${CYTHON_LANGUAGE}")
208119
elseif("C" IN_LIST _languages)
209120
# If only C is enabled globally, assume C
210-
set(_language "C")
121+
set(CYTHON_LANGUAGE C)
211122
elseif("CXX" IN_LIST _languages)
212123
# Likewise for CXX
213-
set(_language "CXX")
124+
set(CYTHON_LANGUAGE CXX)
214125
else()
215126
message(FATAL_ERROR "LANGUAGE keyword required if neither C nor CXX enabled globally")
216127
endif()
217128
endif()
218129

219-
if(NOT _language MATCHES "^(C|CXX)$")
220-
message(FATAL_ERROR "Cython_transpile LANGUAGE must be one of C or CXX")
130+
# Default to C if not found
131+
if(NOT CYTHON_LANGUAGE)
132+
set(CYTHON_LANGUAGE C)
133+
endif()
134+
135+
if(CYTHON_LANGUAGE STREQUAL C)
136+
set(language_arg "")
137+
set(language_ext ".c")
138+
elseif(CYTHON_LANGUAGE STREQUAL CXX)
139+
set(language_arg "--cplus")
140+
set(language_ext ".cxx")
141+
else()
142+
message(FATAL_ERROR "cython_compile_pyx LANGUAGE must be one of C or CXX")
221143
endif()
222144

223145
# Place the cython files in the current binary dir if no path given
224-
if(NOT _args_OUTPUT)
225-
_set_output(${_source_file} ${_language} _args_OUTPUT)
226-
elseif(NOT IS_ABSOLUTE ${_args_OUTPUT})
227-
set(_args_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${_args_OUTPUT}")
146+
# Can use cmake_path for CMake 3.20+
147+
if(NOT CYTHON_OUTPUT)
148+
get_filename_component(basename "${INPUT}" NAME_WE)
149+
150+
set(CYTHON_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${basename}${language_ext}")
151+
elseif(NOT IS_ABSOLUTE CYTHON_OUTPUT)
152+
set(CYTHON_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CYTHON_OUTPUT}")
228153
endif()
229154

230-
set(generated_file ${_args_OUTPUT})
231-
_transpile(${_source_file} ${generated_file} ${_language})
232-
list(APPEND generated_files ${generated_file})
155+
# Normalize the input path
156+
get_filename_component(INPUT "${INPUT}" ABSOLUTE)
157+
set_source_files_properties("${INPUT}" PROPERTIES GENERATED TRUE)
233158

234159
# Output variable only if set
235-
if(_args_OUTPUT_VARIABLE)
236-
set(_output_variable ${_args_OUTPUT_VARIABLE})
237-
set(${_output_variable} ${generated_files} PARENT_SCOPE)
160+
if(CYTHON_OUTPUT_VARIABLE)
161+
set(${CYTHON_OUTPUT_VARIABLE} "${CYTHON_OUTPUT}" PARENT_SCOPE)
238162
endif()
239163

164+
# Generated depfile is expected to have the ".dep" extension and be located
165+
# along side the generated source file.
166+
set(depfile_path "${CYTHON_OUTPUT}.dep")
167+
168+
# Pretty-printed output name
169+
file(RELATIVE_PATH generated_file_relative "${CMAKE_BINARY_DIR}" "${CYTHON_OUTPUT}")
170+
file(RELATIVE_PATH input_file_relative "${CMAKE_SOURCE_DIR}" "${INPUT}")
171+
172+
# Add the command to run the compiler.
173+
add_custom_command(
174+
OUTPUT
175+
"${CYTHON_OUTPUT}"
176+
"${depfile_path}"
177+
COMMAND
178+
${_cython_command}
179+
${language_arg}
180+
${CYTHON_CYTHON_ARGS}
181+
--depfile
182+
"${INPUT}"
183+
--output-file "${CYTHON_OUTPUT}"
184+
MAIN_DEPENDENCY
185+
"${INPUT}"
186+
DEPFILE
187+
"${depfile_path}"
188+
VERBATIM
189+
COMMENT
190+
"Cythonizing source ${input_file_relative} to output ${generated_file_relative}"
191+
)
192+
193+
endfunction()
194+
195+
function(_cython_compute_language OUTPUT_VARIABLE FILENAME)
196+
file(READ "${FILENAME}" FILE_CONTENT)
197+
set(REGEX_PATTERN [=[^[[:space:]]*#[[:space:]]*distutils:.*language[[:space:]]*=[[:space:]]*(c\\+\\+|c)]=])
198+
string(REGEX MATCH "${REGEX_PATTERN}" MATCH_RESULT "${FILE_CONTENT}")
199+
string(TOUPPER "${MATCH_RESULT}" LANGUAGE_NAME)
200+
string(REPLACE "+" "X" LANGUAGE_NAME "${LANGUAGE_NAME}")
201+
set(${OUTPUT_VARIABLE} ${LANGUAGE_NAME} PARENT_SCOPE)
240202
endfunction()
241203

242204
function(_cython_compute_language OUTPUT_VARIABLE FILENAME)

0 commit comments

Comments
 (0)