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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ cython_transpile(<pyx_file>
[MODULE_NAME <module_name>]
[OUTPUT <OutputFile>]
[OUTPUT_VARIABLE <OutputVariable>]
[PUBLIC_HEADER_VARIABLE <PublicHeaderVariable>]
[API_HEADER_VARIABLE <ApiHeaderVariable>]
[DEPENDS <depends> ...]
)
```
Expand All @@ -60,6 +62,13 @@ filename and can only qualify it with the containing package if it can walk
`__init__` files from the source location — set this explicitly when that is not
the case, since it affects `__module__`, pickling, and error messages.

A `.pyx` with `cdef public` or `cdef api` declarations makes Cython write a
`<name>.h` or `<name>_api.h` header next to the generated source. Pass
`PUBLIC_HEADER_VARIABLE` and/or `API_HEADER_VARIABLE` to declare those headers
as build outputs (so other targets can depend on them) and receive their paths
in the given variables. This is useful when embedding Cython in a larger C/C++
application.

`cimport`ed `.pxd` files are tracked automatically through the depfile. Use
`DEPENDS` for extra files or targets the transpilation needs but that do not
exist at configure time, such as a `.pxd` generated by another target (e.g.
Expand Down
62 changes: 57 additions & 5 deletions src/cython_cmake/cmake/UseCython.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# [MODULE_NAME <module_name>]
# [OUTPUT <OutputFile>]
# [OUTPUT_VARIABLE <OutputVariable>]
# [PUBLIC_HEADER_VARIABLE <PublicHeaderVariable>]
# [API_HEADER_VARIABLE <ApiHeaderVariable>]
# [DEPENDS <depends> ...])
#
# Options:
Expand Down Expand Up @@ -55,11 +57,27 @@
# Set the variable ``<OutputVariable>`` in the parent scope to the path to the
# generated source file.
#
# ``PUBLIC_HEADER_VARIABLE <PublicHeaderVariable>``
# A ``.pyx`` with ``cdef public`` declarations makes Cython write a
# ``<name>.h`` header next to the generated source. Pass this to declare that
# header as an additional output (so other targets can depend on it) and set
# ``<PublicHeaderVariable>`` in the parent scope to its path.
#
# ``API_HEADER_VARIABLE <ApiHeaderVariable>``
# As ``PUBLIC_HEADER_VARIABLE``, but for the ``<name>_api.h`` header Cython
# writes for ``cdef api`` declarations.
#
# Defined variables:
#
# ``<OutputVariable>``
# The path of the generated source file.
#
# ``<PublicHeaderVariable>``
# The path of the generated ``cdef public`` header, if requested.
#
# ``<ApiHeaderVariable>``
# The path of the generated ``cdef api`` header, if requested.
#
# Usage example:
#
# .. code-block:: cmake
Expand Down Expand Up @@ -115,7 +133,9 @@ function(_transpile _source_file generated_file language)
message(FATAL_ERROR "_transpile language must be one of C or CXX")
endif()

set_source_files_properties(${generated_file} PROPERTIES GENERATED TRUE)
# _header_outputs (read from the caller's scope) holds any cdef public / api
# headers Cython writes as a side effect; declare them as extra outputs.
set_source_files_properties(${generated_file} ${_header_outputs} PROPERTIES GENERATED TRUE)

if(_args_MODULE_NAME)
set(_module_name_arg "--module-name" "${_args_MODULE_NAME}")
Expand Down Expand Up @@ -150,14 +170,16 @@ function(_transpile _source_file generated_file language)
# and so it still matches inside a "$<1:...>" generator expression.
set(_byproducts "")
if("${_args_CYTHON_ARGS}" MATCHES "(^|[^A-Za-z0-9_-])(-a|--annotate|--annotate-fullc|--annotate-coverage=[^;]*)([^A-Za-z0-9_-]|$)")
get_filename_component(_generated_we "${generated_file}" NAME_WE)
set(_byproducts "${output_directory}/${_generated_we}.html")
# Cython strips only the final extension when naming the HTML file.
get_filename_component(_generated_name "${generated_file}" NAME)
string(REGEX REPLACE "\\.[^.]*$" "" _generated_stem "${_generated_name}")
set(_byproducts "${output_directory}/${_generated_stem}.html")
endif()

# Add the command to run the compiler.
# Keep _args_CYTHON_ARGS quoted: a generator expression can contain ';'
add_custom_command(
OUTPUT "${generated_file}"
OUTPUT "${generated_file}" ${_header_outputs}
COMMAND
"${CMAKE_COMMAND}" -E make_directory "${output_directory}"
COMMAND
Expand Down Expand Up @@ -213,7 +235,7 @@ endfunction()

function(Cython_transpile)
set(_options )
set(_one_value LANGUAGE MODULE_NAME OUTPUT OUTPUT_VARIABLE)
set(_one_value LANGUAGE MODULE_NAME OUTPUT OUTPUT_VARIABLE PUBLIC_HEADER_VARIABLE API_HEADER_VARIABLE)
set(_multi_value CYTHON_ARGS INCLUDE_DIRECTORIES DEPENDS)

cmake_parse_arguments(_args
Expand Down Expand Up @@ -291,6 +313,28 @@ function(Cython_transpile)
endif()

set(generated_file "${_args_OUTPUT}")

# Cython writes cdef public / api headers next to the generated source, named
# after its basename. Declare the requested ones as extra outputs (via
# _header_outputs, read by _transpile) so downstream targets can depend on
# them, and hand their paths back to the caller.
set(_header_outputs)
if(_args_PUBLIC_HEADER_VARIABLE OR _args_API_HEADER_VARIABLE)
get_filename_component(_header_dir "${generated_file}" DIRECTORY)
get_filename_component(_header_stem "${generated_file}" NAME)
# Cython strips only the final extension (os.path.splitext), so
# "mod.generated.c" yields "mod.generated.h"; NAME_WE would strip both.
string(REGEX REPLACE "\\.[^.]*$" "" _header_stem "${_header_stem}")
endif()
if(_args_PUBLIC_HEADER_VARIABLE)
set(_public_header "${_header_dir}/${_header_stem}.h")
list(APPEND _header_outputs "${_public_header}")
endif()
if(_args_API_HEADER_VARIABLE)
set(_api_header "${_header_dir}/${_header_stem}_api.h")
list(APPEND _header_outputs "${_api_header}")
endif()

_transpile("${_source_file}" "${generated_file}" ${_language})
list(APPEND generated_files "${generated_file}")

Expand All @@ -300,6 +344,14 @@ function(Cython_transpile)
set(${_output_variable} "${generated_files}" PARENT_SCOPE)
endif()

if(_args_PUBLIC_HEADER_VARIABLE)
set(${_args_PUBLIC_HEADER_VARIABLE} "${_public_header}" PARENT_SCOPE)
endif()

if(_args_API_HEADER_VARIABLE)
set(${_args_API_HEADER_VARIABLE} "${_api_header}" PARENT_SCOPE)
endif()

endfunction()

function(_cython_compute_language OUTPUT_VARIABLE FILENAME)
Expand Down
176 changes: 176 additions & 0 deletions tests/packages/public_headers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml

# ruff
.ruff_cache/

# LSP config files
pyrightconfig.json

# End of https://www.toptal.com/developers/gitignore/api/python
32 changes: 32 additions & 0 deletions tests/packages/public_headers/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.15...3.29)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)

find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
find_package(Cython MODULE REQUIRED VERSION 3.0)
include(UseCython)

# mymath.pyx has cdef public / cdef api declarations, so Cython writes
# mymath.h / mymath_api.h next to the generated source. Ask for their paths.
cython_transpile(mymath.pyx
LANGUAGE C
OUTPUT_VARIABLE mymath_c
PUBLIC_HEADER_VARIABLE mymath_h
API_HEADER_VARIABLE mymath_api_h
)

# A separate target consuming the generated public header. The explicit
# OBJECT_DEPENDS on the (now declared) header output makes the build wait for
# Cython to run and rebuild the consumer when the header changes.
add_library(consumer STATIC consumer.c)
target_link_libraries(consumer PRIVATE Python::Module)
target_include_directories(consumer PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
set_target_properties(consumer PROPERTIES POSITION_INDEPENDENT_CODE ON)
set_source_files_properties(consumer.c PROPERTIES OBJECT_DEPENDS "${mymath_h}")

python_add_library(mymath MODULE "${mymath_c}" WITH_SOABI)
target_link_libraries(mymath PRIVATE consumer)

install(TARGETS mymath DESTINATION .)
6 changes: 6 additions & 0 deletions tests/packages/public_headers/consumer.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// A target other than the extension module, consuming the cdef public header
// Cython generates for mymath.pyx. It #includes the header and calls the
// exported function, proving the header is a real, dependable build output.
#include "mymath.h"

int consumer_add_one(int x) { return add_one(x); }
1 change: 1 addition & 0 deletions tests/packages/public_headers/main.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
summary: cdef public / api headers consumed by another target
7 changes: 7 additions & 0 deletions tests/packages/public_headers/mymath.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# cython: language_level=3

cdef public int add_one(int x):
return x + 1

cdef api int add_two(int x):
return x + 2
7 changes: 7 additions & 0 deletions tests/packages/public_headers/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build-system]
requires = ["scikit-build-core", "cython", "cython-cmake"]
build-backend = "scikit_build_core.build"

[project]
name = "example"
version = "0.0.1"
Loading
Loading