Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 54 additions & 30 deletions src/cython_cmake/cmake/UseCython.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
# Options:
#
# ``LANGUAGE [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.
# Force the generation of either a C or C++ file. Recommended; will attempt
# to be deduced if not specified, defaults to C unless only CXX is enabled.
#
# ``CYTHON_ARGS <args>``
# Specify additional arguments for the cythonization process. Will default to
Expand Down Expand Up @@ -109,31 +108,15 @@ function(Cython_compile_pyx)
message(FATAL_ERROR "One and only one input file must be specified, got '${_source_files}'")
endif()

# Set target language
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
set(_language ${_args_LANGUAGE})
if(NOT _language)
if("C" IN_LIST _languages)
set(_language "C")
elseif("CXX" IN_LIST _languages)
set(_language "CXX")
endif()
else()
if(NOT _language MATCHES "^(C|CXX)$")
message(FATAL_ERROR "LANGUAGE must be one of C or CXX")
endif()
endif()

set(_language_C_arg "")
set(_language_C_extension "c")
set(_language_CXX_arg "--cplus")
set(_language_CXX_extension "cxx")

set(_language_arg ${_language_${_language}_arg})
set(_language_extension ${_language_${_language}_extension})

function(_compile_pyx _source_file generated_file)
function(_compile_pyx _source_file generated_file language)

if(language STREQUAL "C")
set(_language_arg "")
elseif(language STREQUAL "CXX")
set(_language_arg "--cplus")
else()
message(FATAL_ERROR "_compile_pyx language must be one of C or CXX")
endif()

set_source_files_properties(${generated_file} PROPERTIES GENERATED TRUE)

Expand Down Expand Up @@ -168,7 +151,15 @@ function(Cython_compile_pyx)
)
endfunction()

function(_set_output _input_file _output_var)
function(_set_output _input_file _language _output_var)
if(_language STREQUAL "C")
set(_language_extension "c")
elseif(_language STREQUAL "CXX")
set(_language_extension "cxx")
else()
message(FATAL_ERROR "_set_output language must be one of C or CXX")
endif()

# Can use cmake_path for CMake 3.20+
# cmake_path(GET _input_file STEM basename)
get_filename_component(_basename "${_input_file}" NAME_WE)
Expand All @@ -180,15 +171,37 @@ function(Cython_compile_pyx)

list(GET _source_files 0 _source_file)

# Set target language
set(_language ${_args_LANGUAGE})
if(NOT _language)
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
if("C" IN_LIST _languages AND "CXX" IN_LIST _languages)
# Try to compute language. Returns falsy if not found.
_cython_compute_language(_language ${_source_file})
elseif("C" IN_LIST _languages)
# If only C is enabled globally, assume C
set(_language "C")
elseif("CXX" IN_LIST _languages)
# Likewise for CXX
set(_language "CXX")
else()
message(FATAL_ERROR "LANGUAGE keyword required if neither C nor CXX enabled globally")
endif()
endif()

if(NOT _language MATCHES "^(C|CXX)$")
message(FATAL_ERROR "cython_compile_pyx LANGUAGE must be one of C or CXX")
endif()

# Place the cython files in the current binary dir if no path given
if(NOT _args_OUTPUT)
_set_output(${_source_file} _args_OUTPUT)
_set_output(${_source_file} ${_language} _args_OUTPUT)
elseif(NOT IS_ABSOLUTE ${_args_OUTPUT})
set(_args_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${_args_OUTPUT}")
endif()

set(generated_file ${_args_OUTPUT})
_compile_pyx(${_source_file} ${generated_file})
_compile_pyx(${_source_file} ${generated_file} ${_language})
list(APPEND generated_files ${generated_file})

# Output variable only if set
Expand All @@ -198,3 +211,14 @@ function(Cython_compile_pyx)
endif()

endfunction()

function(_cython_compute_language OUTPUT_VARIABLE FILENAME)
file(READ "${FILENAME}" FILE_CONTENT)
# Check for compiler directive similar to "# distutils: language = c++"
# See https://cython.readthedocs.io/en/latest/src/userguide/wrapping_CPlusPlus.html#declare-a-var-with-the-wrapped-c-class
set(REGEX_PATTERN [=[^[[:space:]]*#[[:space:]]*distutils:.*language[[:space:]]*=[[:space:]]*(c\\+\\+|c)]=])
string(REGEX MATCH "${REGEX_PATTERN}" MATCH_RESULT "${FILE_CONTENT}")
string(TOUPPER "${MATCH_RESULT}" LANGUAGE_NAME)
string(REPLACE "+" "X" LANGUAGE_NAME "${LANGUAGE_NAME}")
set(${OUTPUT_VARIABLE} ${LANGUAGE_NAME} PARENT_SCOPE)
endfunction()
56 changes: 56 additions & 0 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,59 @@ def test_output_argument(monkeypatch, tmp_path, output_arg):
build_files = {x.name for x in build_dir.iterdir()}
assert f"{generated_file}.dep" in build_files
assert generated_file in build_files


def test_implicit_cxx(monkeypatch, tmp_path):
package_dir = tmp_path / "pkg3"
shutil.copytree(DIR / "packages/simple", package_dir)
monkeypatch.chdir(package_dir)

cmakelists = Path("CMakeLists.txt")
txt = (
cmakelists.read_text()
.replace("LANGUAGE C", "")
.replace("LANGUAGES C", "LANGUAGES CXX")
)
cmakelists.write_text(txt)

wheel = build_wheel(
str(tmp_path), {"build-dir": "build", "wheel.license-files": []}
)

with zipfile.ZipFile(tmp_path / wheel) as f:
file_names = set(f.namelist())
assert len(file_names) == 4

build_files = {x.name for x in Path("build").iterdir()}
assert "simple.cxx.dep" in build_files
assert "simple.cxx" in build_files


def test_directive_cxx(monkeypatch, tmp_path):
package_dir = tmp_path / "pkg4"
shutil.copytree(DIR / "packages/simple", package_dir)
monkeypatch.chdir(package_dir)

cmakelists = Path("CMakeLists.txt")
txt = (
cmakelists.read_text()
.replace("LANGUAGE C", "")
.replace("LANGUAGES C", "LANGUAGES CXX")
)
cmakelists.write_text(txt)

simple = Path("simple.pyx")
txt = simple.read_text()
simple.write_text(f"# distutils: language=c++\n{txt}")

wheel = build_wheel(
str(tmp_path), {"build-dir": "build", "wheel.license-files": []}
)

with zipfile.ZipFile(tmp_path / wheel) as f:
file_names = set(f.namelist())
assert len(file_names) == 4

build_files = {x.name for x in Path("build").iterdir()}
assert "simple.cxx.dep" in build_files
assert "simple.cxx" in build_files