Skip to content

Commit f81df33

Browse files
henryiiijcfr
authored andcommitted
feat: deduce C/CXX if possible
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 63cb870 commit f81df33

File tree

2 files changed

+105
-14
lines changed

2 files changed

+105
-14
lines changed

src/cython_cmake/cmake/UseCython.cmake

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# using cython.
99
#
1010
# Cython_compile_pyx(<pyx_file>
11-
# LANGUAGE C | CXX
11+
# [LANGUAGE C | CXX]
1212
# [CYTHON_ARGS <args> ...]
1313
# [OUTPUT <OutputFile>]
1414
# [OUTPUT_VARIABLE <OutputVariable>]
@@ -17,7 +17,8 @@
1717
# Options:
1818
#
1919
# ``LANGUAGE [C | CXX]``
20-
# Force the generation of either a C or C++ file. Required.
20+
# Force the generation of either a C or C++ file. Recommended; will attempt
21+
# to be deduced if not specified, defaults to C unless only CXX is enabled.
2122
#
2223
# ``CYTHON_ARGS <args>``
2324
# Specify additional arguments for the cythonization process. Will default to
@@ -75,14 +76,21 @@ if(CMAKE_VERSION VERSION_LESS "3.7")
7576
message(FATAL_ERROR "CMake 3.7 required for DEPFILE")
7677
endif()
7778

78-
function(Cython_compile_pyx INPUT)
79+
80+
function(Cython_compile_pyx)
7981
cmake_parse_arguments(
80-
PARSE_ARGV 1
82+
PARSE_ARGV 0
8183
CYTHON
8284
""
8385
"OUTPUT;LANGUAGE;OUTPUT_VARIABLE"
8486
"CYTHON_ARGS"
8587
)
88+
set(ALL_INPUT ${CYTHON_UNPARSED_ARGUMENTS})
89+
list(LENGTH ALL_INPUT INPUT_LENGTH)
90+
if(NOT INPUT_LENGTH EQUAL 1)
91+
message(FATAL_ERROR "One and only one input file must be specified, got '${ALL_INPUT}'")
92+
endif()
93+
list(GET ALL_INPUT 0 INPUT)
8694

8795
if(DEFINED CYTHON_EXECUTABLE)
8896
set(_cython_command "${CYTHON_EXECUTABLE}")
@@ -99,34 +107,51 @@ function(Cython_compile_pyx INPUT)
99107
set(CYTHON_CYTHON_ARGS "${CYTHON_ARGS}")
100108
endif()
101109

102-
# Set target language (required)
110+
# Set target language
111+
if(NOT CYTHON_LANGUAGE)
112+
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
113+
114+
if("C" IN_LIST _languages AND "CXX" IN_LIST _languages)
115+
# Try to compute language. Returns falsy if not found.
116+
_cython_compute_language(CYTHON_LANGUAGE ${INPUT})
117+
message(STATUS "${CYTHON_LANGUAGE}")
118+
elseif("C" IN_LIST _languages)
119+
# If only C is enabled globally, assume C
120+
set(CYTHON_LANGUAGE C)
121+
elseif("CXX" IN_LIST _languages)
122+
# Likewise for CXX
123+
set(CYTHON_LANGUAGE CXX)
124+
else()
125+
message(FATAL_ERROR "LANGUAGE keyword required if neither C nor CXX enabled globally")
126+
endif()
127+
endif()
128+
129+
# Default to C if not found
103130
if(NOT CYTHON_LANGUAGE)
104-
message(SEND_ERROR "cython_compile_pyx LANGUAGE keyword is required")
105-
elseif(CYTHON_LANGUAGE STREQUAL C)
131+
set(CYTHON_LANGUAGE C)
132+
endif()
133+
134+
if(CYTHON_LANGUAGE STREQUAL C)
106135
set(language_arg "")
107136
set(language_ext ".c")
108137
elseif(CYTHON_LANGUAGE STREQUAL CXX)
109138
set(language_arg "--cplus")
110139
set(language_ext ".cxx")
111140
else()
112-
message(SEND_ERROR "cython_compile_pyx LANGUAGE must be one of C or CXX")
141+
message(FATAL_ERROR "cython_compile_pyx LANGUAGE must be one of C or CXX")
113142
endif()
114143

115144
# Place the cython files in the current binary dir if no path given
116145
# Can use cmake_path for CMake 3.20+
117146
if(NOT CYTHON_OUTPUT)
118-
# cmake_path(GET INPUT STEM basename)
119147
get_filename_component(basename "${INPUT}" NAME_WE)
120148

121-
# cmake_path(APPEND CMAKE_CURRENT_BINARY_DIR "${basename}${language_ext}" OUTPUT_VARIABLE CYTHON_OUTPUT)
122149
set(CYTHON_OUPUT "${CMAKE_CURRENT_BINARY_DIR}/${basename}${language_ext}")
123150
endif()
124151

125-
# cmake_path(ABSOLUTE_PATH CYTHON_OUTPUT)
126152
get_filename_component(CYTHON_OUTPUT "${CYTHON_OUPUT}" ABSOLUTE)
127153

128154
# Normalize the input path
129-
# cmake_path(ABSOLUTE_PATH INPUT)
130155
get_filename_component(INPUT "${INPUT}" ABSOLUTE)
131156
set_source_files_properties("${INPUT}" PROPERTIES GENERATED TRUE)
132157

@@ -155,7 +180,7 @@ function(Cython_compile_pyx INPUT)
155180
--depfile
156181
"${INPUT}"
157182
--output-file "${CYTHON_OUTPUT}"
158-
DEPENDS
183+
MAIN_DEPENDENCY
159184
"${INPUT}"
160185
DEPFILE
161186
"${depfile_path}"
@@ -165,3 +190,12 @@ function(Cython_compile_pyx INPUT)
165190
)
166191

167192
endfunction()
193+
194+
function(_cython_compute_language OUTPUT_VARIABLE FILENAME)
195+
file(READ "${FILENAME}" FILE_CONTENT)
196+
set(REGEX_PATTERN [=[^[[:space:]]*#[[:space:]]*distutils:.*language[[:space:]]*=[[:space:]]*(c\\+\\+|c)]=])
197+
string(REGEX MATCH "${REGEX_PATTERN}" MATCH_RESULT "${FILE_CONTENT}")
198+
string(TOUPPER "${MATCH_RESULT}" LANGUAGE_NAME)
199+
string(REPLACE "+" "X" LANGUAGE_NAME "${LANGUAGE_NAME}")
200+
set(${OUTPUT_VARIABLE} ${LANGUAGE_NAME} PARENT_SCOPE)
201+
endfunction()

tests/test_package.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import importlib.metadata
4+
import shutil
45
import zipfile
56
from pathlib import Path
67

@@ -15,7 +16,7 @@ def test_version():
1516
assert importlib.metadata.version("cython_cmake") == m.__version__
1617

1718

18-
def test_scikit_build_core(monkeypatch, tmp_path):
19+
def test_simple(monkeypatch, tmp_path):
1920
monkeypatch.chdir(DIR / "packages/simple")
2021
build_dir = tmp_path / "build"
2122

@@ -30,3 +31,59 @@ def test_scikit_build_core(monkeypatch, tmp_path):
3031
build_files = {x.name for x in build_dir.iterdir()}
3132
assert "simple.c.dep" in build_files
3233
assert "simple.c" in build_files
34+
35+
36+
def test_implicit_cxx(monkeypatch, tmp_path):
37+
package_dir = tmp_path / "pkg2"
38+
shutil.copytree(DIR / "packages/simple", package_dir)
39+
monkeypatch.chdir(package_dir)
40+
41+
cmakelists = Path("CMakeLists.txt")
42+
txt = (
43+
cmakelists.read_text()
44+
.replace("LANGUAGE C", "")
45+
.replace("LANGUAGES C", "LANGUAGES CXX")
46+
)
47+
cmakelists.write_text(txt)
48+
49+
wheel = build_wheel(
50+
str(tmp_path), {"build-dir": "build", "wheel.license-files": []}
51+
)
52+
53+
with zipfile.ZipFile(tmp_path / wheel) as f:
54+
file_names = set(f.namelist())
55+
assert len(file_names) == 4
56+
57+
build_files = {x.name for x in Path("build").iterdir()}
58+
assert "simple.cxx.dep" in build_files
59+
assert "simple.cxx" in build_files
60+
61+
62+
def test_directive_cxx(monkeypatch, tmp_path):
63+
package_dir = tmp_path / "pkg3"
64+
shutil.copytree(DIR / "packages/simple", package_dir)
65+
monkeypatch.chdir(package_dir)
66+
67+
cmakelists = Path("CMakeLists.txt")
68+
txt = (
69+
cmakelists.read_text()
70+
.replace("LANGUAGE C", "")
71+
.replace("LANGUAGES C", "LANGUAGES CXX")
72+
)
73+
cmakelists.write_text(txt)
74+
75+
simple = Path("simple.pyx")
76+
txt = simple.read_text()
77+
simple.write_text(f"# distutils: language=c++\n{txt}")
78+
79+
wheel = build_wheel(
80+
str(tmp_path), {"build-dir": "build", "wheel.license-files": []}
81+
)
82+
83+
with zipfile.ZipFile(tmp_path / wheel) as f:
84+
file_names = set(f.namelist())
85+
assert len(file_names) == 4
86+
87+
build_files = {x.name for x in Path("build").iterdir()}
88+
assert "simple.cxx.dep" in build_files
89+
assert "simple.cxx" in build_files

0 commit comments

Comments
 (0)