diff --git a/.github/workflows/emscripten.yaml b/.github/workflows/emscripten.yaml new file mode 100644 index 00000000..45b30213 --- /dev/null +++ b/.github/workflows/emscripten.yaml @@ -0,0 +1,28 @@ +name: WASM + +on: + workflow_dispatch: + pull_request: + branches: + - master + - stable + - v* + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-wasm-emscripten: + name: Pyodide wheel + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + + - uses: pypa/cibuildwheel@v2.20 + with: + package-dir: tests + only: cp312-pyodide_wasm32 diff --git a/.gitignore b/.gitignore index 356876a2..511dd17b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ inter_module.dll /src/nanobind/ext /src/nanobind/src /dist +/tests/dist /bench compile_commands.json @@ -30,6 +31,7 @@ cmake_install.cmake \.DS_Store \.cmake __pycache__ +.pyodide-xbuildenv-*/ nanobind.egg-info test_*_ext*.so test_*_ext*.pyd diff --git a/CMakeLists.txt b/CMakeLists.txt index ba81d017..b5ef9034 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,7 +155,8 @@ if (NOT TARGET Python::Module OR NOT TARGET Python::Interpreter) find_package(Python 3.8 REQUIRED COMPONENTS Interpreter ${NB_PYTHON_DEV_MODULE} - OPTIONAL_COMPONENTS Development.SABIModule) + OPTIONAL_COMPONENTS Development.SABIModule + GLOBAL) endif() # --------------------------------------------------------------------------- diff --git a/cmake/nanobind-config.cmake b/cmake/nanobind-config.cmake index ec7ff583..ded1a2da 100644 --- a/cmake/nanobind-config.cmake +++ b/cmake/nanobind-config.cmake @@ -189,6 +189,11 @@ function (nanobind_build_library TARGET_NAME) target_link_options(${TARGET_NAME} PUBLIC $<${NB_OPT_SIZE}:-Wl,--gc-sections>) endif() + if (CMAKE_SYSTEM_NAME MATCHES Emscripten) + target_compile_options(${TARGET_NAME} PUBLIC -fexceptions) + target_link_options(${TARGET_NAME} PUBLIC -fexceptions) + endif() + set_target_properties(${TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) @@ -228,9 +233,10 @@ function (nanobind_build_library TARGET_NAME) ${NB_DIR}/ext/robin_map/include) endif() + get_property(nanobind_python_headers TARGET Python::Module PROPERTY INTERFACE_INCLUDE_DIRECTORIES) target_include_directories(${TARGET_NAME} PUBLIC - ${Python_INCLUDE_DIRS} - ${NB_DIR}/include) + "${nanobind_python_headers}" + "${NB_DIR}/include") target_compile_features(${TARGET_NAME} PUBLIC cxx_std_17) nanobind_set_visibility(${TARGET_NAME}) @@ -275,6 +281,9 @@ function (nanobind_compile_options name) if (MSVC) target_compile_options(${name} PRIVATE $<$:/bigobj /MP>) endif() + if (CMAKE_SYSTEM_NAME MATCHES Emscripten) + target_compile_options(${name} PUBLIC $<$:-fexceptions>) + endif() endfunction() function (nanobind_strip name) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 62f2bdb8..fbb7479b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,11 +1,32 @@ +include_guard(GLOBAL) + +cmake_minimum_required(VERSION 3.15...3.27) +project(nanobind_tests LANGUAGES CXX) + +if(PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME) + set(NB_TESTS ON) + if(APPLE) + set(BASEPOINT @loader_path) + else() + set(BASEPOINT $ORIGIN) + endif() + set(CMAKE_INSTALL_RPATH ${BASEPOINT} ${BASEPOINT}/${CMAKE_INSTALL_LIBDIR}) + + add_subdirectory(.. ./build) +endif() + # If leaks are found, abort() during interpreter shutdown to catch this in the CI add_definitions(-DNB_ABORT_ON_LEAK) +if (EMSCRIPTEN) + set(NB_EXTRA_ARGS ${NB_EXTRA_ARGS} NB_STATIC) +endif() + if (NB_TEST_STABLE_ABI) set(NB_EXTRA_ARGS ${NB_EXTRA_ARGS} STABLE_ABI) endif() -if (NB_TEST_SHARED_BUILD) +if (NB_TEST_SHARED_BUILD AND NOT EMSCRIPTEN) set(NB_EXTRA_ARGS ${NB_EXTRA_ARGS} NB_SHARED) endif() @@ -58,22 +79,26 @@ foreach (NAME functions classes ndarray stl enum typing make_iterator) set(PYI_PREFIX $/) endif() - nanobind_add_stub( - ${NAME}_ext_stub - MODULE test_${NAME}_ext - OUTPUT ${PYI_PREFIX}test_${NAME}_ext.pyi - PYTHON_PATH $ - DEPENDS test_${NAME}_ext - ${EXTRA}) + if(NOT CMAKE_CROSSCOMPILING) + nanobind_add_stub( + ${NAME}_ext_stub + MODULE test_${NAME}_ext + OUTPUT ${PYI_PREFIX}test_${NAME}_ext.pyi + PYTHON_PATH $ + DEPENDS test_${NAME}_ext + ${EXTRA}) + endif() endforeach() -nanobind_add_stub( - py_stub - MODULE py_stub_test - OUTPUT ${PYI_PREFIX}py_stub_test.pyi - PYTHON_PATH $ - DEPENDS py_stub_test.py -) +if(NOT CMAKE_CROSSCOMPILING) + nanobind_add_stub( + py_stub + MODULE py_stub_test + OUTPUT ${PYI_PREFIX}py_stub_test.pyi + PYTHON_PATH $ + DEPENDS py_stub_test.py + ) +endif() find_package (Eigen3 3.3.1 NO_MODULE) if (TARGET Eigen3::Eigen) @@ -97,6 +122,17 @@ nanobind_add_module(test_inter_module_2_ext NB_DOMAIN mydomain test_inter_module target_link_libraries(test_inter_module_1_ext PRIVATE inter_module) target_link_libraries(test_inter_module_2_ext PRIVATE inter_module) +set(TEST_PYI_FILES + test_functions_ext.pyi + test_stl_ext.pyi + test_typing_ext.pyi + test_enum_ext.pyi + test_ndarray_ext.pyi + test_make_iterator_ext.pyi + test_classes_ext.pyi + py_stub_test.pyi +) + set(TEST_FILES common.py test_classes.py @@ -116,19 +152,13 @@ set(TEST_FILES test_ndarray.py test_stubs.py test_typing.py - - # Stub reference files - test_classes_ext.pyi.ref - test_functions_ext.pyi.ref - test_make_iterator_ext.pyi.ref - test_ndarray_ext.pyi.ref - test_stl_ext.pyi.ref - test_enum_ext.pyi.ref - test_typing_ext.pyi.ref py_stub_test.py - py_stub_test.pyi.ref ) +# Stub reference files +list(TRANSFORM TEST_PYI_FILES APPEND ".ref" OUTPUT_VARIABLE TEST_REF_FILES) +list(APPEND TEST_FILES ${TEST_REF_FILES}) + if (NOT (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) OR MSVC) if (CMAKE_CONFIGURATION_TYPES) set(OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/$) @@ -147,3 +177,37 @@ if (NOT (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) OR MSVC) add_custom_target(copy-tests ALL DEPENDS ${TEST_FILES_OUT}) endif() + +if(DEFINED SKBUILD) + if(NOT CMAKE_CROSSCOMPILING) + list(TRANSFORM TEST_PYI_FILES PREPEND "${CMAKE_CURRENT_BINARY_DIR}/" OUTPUT_VARIABLE TEST_OUT_FILES) + install( + FILES ${TEST_OUT_FILES} + DESTINATION . + ) + endif() + install( + TARGETS + inter_module + test_inter_module_1_ext + test_inter_module_2_ext + test_functions_ext + test_classes_ext + test_holders_ext + test_stl_ext + test_bind_map_ext + test_bind_vector_ext + test_chrono_ext + test_enum_ext + test_eval_ext + test_ndarray_ext + test_intrusive_ext + test_exception_ext + test_make_iterator_ext + test_typing_ext + test_issue_ext + DESTINATION + . + ) +endif() + diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 00000000..dc9c949a --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,24 @@ +# Warning: this is currently used for pyodide, and is not a general out-of-tree +# builder for the tests (yet). Specifically, wheels can't be built from SDists. + +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "nanobind_tests" +version = "0.0.1" +dependencies = ["pytest", "pytest-timeout", "numpy", "scipy"] +classifiers = [ + "Private :: Do Not Upload", +] + +[tool.scikit-build] +minimum-version = "build-system.requires" + +[tool.cibuildwheel] +test-command = "pytest -o timeout=0 -p no:cacheprovider {project}/tests/test_*.py" + +[[tool.cibuildwheel.overrides]] +select = ["*-pyodide_wasm32"] +environment.PYODIDE_BUILD_EXPORTS = "whole_archive" diff --git a/tests/test_stubs.py b/tests/test_stubs.py index c6e1121f..7187846b 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -5,7 +5,10 @@ import platform import pytest -is_unsupported = platform.python_implementation() == 'PyPy' or sys.version_info < (3, 10) +import test_typing_ext +LIB_DIR = pathlib.Path(test_typing_ext.__file__).parent.resolve() + +is_unsupported = platform.python_implementation() == 'PyPy' or sys.version_info < (3, 10) or sys.platform.startswith("emscripten") skip_on_unsupported = pytest.mark.skipif( is_unsupported, reason="Stub generation is only tested on CPython >= 3.10.0") @@ -33,7 +36,7 @@ def test01_check_stub_refs(p_ref): """ Check that generated stub files match reference input """ - p_in = p_ref.with_suffix('') + p_in = LIB_DIR / p_ref.with_suffix('').name with open(p_ref, 'r') as f: s_ref = f.read().split('\n') with open(p_in, 'r') as f: