diff --git a/src/cython_cmake/cmake/UseCython.cmake b/src/cython_cmake/cmake/UseCython.cmake index 12ac109..a63ad16 100644 --- a/src/cython_cmake/cmake/UseCython.cmake +++ b/src/cython_cmake/cmake/UseCython.cmake @@ -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 `` # Specify additional arguments for the cythonization process. Will default to @@ -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) @@ -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) @@ -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 @@ -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() diff --git a/tests/test_package.py b/tests/test_package.py index f7ddacb..dedfb8a 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -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