diff --git a/cmake/nanobind-config.cmake b/cmake/nanobind-config.cmake index 8e204d63..78590100 100644 --- a/cmake/nanobind-config.cmake +++ b/cmake/nanobind-config.cmake @@ -590,7 +590,7 @@ endfunction() # --------------------------------------------------------------------------- function (nanobind_add_stub name) - cmake_parse_arguments(PARSE_ARGV 1 ARG "VERBOSE;INCLUDE_PRIVATE;EXCLUDE_DOCSTRINGS;EXCLUDE_VALUES;INSTALL_TIME;RECURSIVE;EXCLUDE_FROM_ALL" "MODULE;COMPONENT;PATTERN_FILE;OUTPUT_PATH" "PYTHON_PATH;DEPENDS;MARKER_FILE;OUTPUT") + cmake_parse_arguments(PARSE_ARGV 1 ARG "VERBOSE;INCLUDE_PRIVATE;EXCLUDE_DOCSTRINGS;EXCLUDE_VALUES;INSTALL_TIME;RECURSIVE;EXCLUDE_FROM_ALL" "MODULE;COMPONENT;PATTERN_FILE;OUTPUT_PATH" "PYTHON_PATH;LIB_PATH;DEPENDS;MARKER_FILE;OUTPUT") if (EXISTS ${NB_DIR}/src/stubgen.py) set(NB_STUBGEN "${NB_DIR}/src/stubgen.py") @@ -626,6 +626,10 @@ function (nanobind_add_stub name) list(APPEND NB_STUBGEN_ARGS -i "${PYTHON_PATH}") endforeach() + foreach (LIB_PATH IN LISTS ARG_LIB_PATH) + list(APPEND NB_STUBGEN_ARGS -L "${LIB_PATH}") + endforeach() + if (ARG_PATTERN_FILE) list(APPEND NB_STUBGEN_ARGS -p "${ARG_PATTERN_FILE}") endif() diff --git a/docs/api_cmake.rst b/docs/api_cmake.rst index 13b492e5..7def2ee9 100644 --- a/docs/api_cmake.rst +++ b/docs/api_cmake.rst @@ -492,6 +492,18 @@ Nanobind's CMake tooling includes a convenience command to interface with the generation. Otherwise, generator expressions should not be used. Optional. + * - ``LIB_PATH`` + - List of search paths that should be considered when searching for + shared libraries. This can be useful when the Python module being + imported depends on shared libraries that are not in the default + search path. On Windows, these directories are added to the DLL search + path using ``os.add_dll_directory()``. On Linux, the paths are + prepended to the ``LD_LIBRARY_PATH`` environment variable (on macOS, + to ``DYLD_LIBRARY_PATH``). The paths are relative to + ``CMAKE_CURRENT_BINARY_DIR`` for build-time stub generation and + relative to ``CMAKE_INSTALL_PREFIX`` for install-time stub + generation. Optional. + * - ``DEPENDS`` - Any targets listed here will be marked as a dependencies. This should generally be used to list the target names of one or more prior diff --git a/docs/typing.rst b/docs/typing.rst index 6c9d8a6f..a3e2a3eb 100644 --- a/docs/typing.rst +++ b/docs/typing.rst @@ -539,8 +539,9 @@ The program has the following command line options: .. code-block:: text - usage: python -m nanobind.stubgen [-h] [-o FILE] [-O PATH] [-i PATH] [-m MODULE] - [-r] [-M FILE] [-P] [-D] [--exclude-values] [-q] + usage: python -m nanobind.stubgen [-h] [-o FILE] [-O PATH] [-i PATH] + [-L PATH] [-m MODULE] [-r] [-M FILE] [-P] + [-D] [--exclude-values] [-q] Generate stubs for nanobind-based extensions. @@ -550,6 +551,8 @@ The program has the following command line options: -O PATH, --output-dir PATH write generated stubs to the specified directory -i PATH, --import PATH add the directory to the Python import path (can specify multiple times) + -L PATH, --lib-path PATH add directory to shared library search path (can + specify multiple times) -m MODULE, --module MODULE generate a stub for the specified module (can specify multiple times) -r, --recursive recursively process submodules diff --git a/src/stubgen.py b/src/stubgen.py index 67fbddc9..e551a24d 100755 --- a/src/stubgen.py +++ b/src/stubgen.py @@ -1285,6 +1285,16 @@ def parse_options(args: List[str]) -> argparse.Namespace: help="add the directory to the Python import path (can specify multiple times)", ) + parser.add_argument( + "-L", + "--lib-path", + action="append", + metavar="PATH", + dest="lib_paths", + default=[], + help="add directory to shared library search path (can specify multiple times)" + ) + parser.add_argument( "-m", "--module", @@ -1426,6 +1436,7 @@ def add_pattern(query: str, lines: List[str]): def main(args: Optional[List[str]] = None) -> None: import sys + import os # Ensure that the current directory is on the path if "" not in sys.path and "." not in sys.path: @@ -1446,6 +1457,16 @@ def main(args: Optional[List[str]] = None) -> None: for i in opt.imports: sys.path.insert(0, i) + if os.name == 'nt': + for lib_path in opt.lib_paths: + os.add_dll_directory(lib_path) + else: + lib_env = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" + old_value = os.environ.get(lib_env, "") + paths_str = ":".join(opt.lib_paths) + if paths_str: + os.environ[lib_env] = f"{paths_str}:{old_value}" if old_value else paths_str + for i, mod in enumerate(opt.modules): if not opt.quiet: if i > 0: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6a66f534..9a5ed1fd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -143,11 +143,27 @@ target_compile_definitions(inter_module PRIVATE -DSHARED_BUILD) target_compile_features(inter_module PRIVATE cxx_std_17) target_include_directories(inter_module PRIVATE ${NB_DIR}/include) +# Output inter_module to a separate lib subdirectory to test LIB_PATH functionality +set_target_properties(inter_module PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib +) + nanobind_add_module(test_inter_module_1_ext NB_DOMAIN mydomain test_inter_module_1.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_inter_module_2_ext NB_DOMAIN mydomain test_inter_module_2.cpp ${NB_EXTRA_ARGS}) target_link_libraries(test_inter_module_1_ext PRIVATE inter_module) target_link_libraries(test_inter_module_2_ext PRIVATE inter_module) +# Generate stub for inter_module_1 to test LIB_PATH functionality +nanobind_add_stub( + inter_module_1_ext_stub + MODULE test_inter_module_1_ext + OUTPUT ${PYI_PREFIX}test_inter_module_1_ext.pyi + PYTHON_PATH $ + LIB_PATH $ + DEPENDS test_inter_module_1_ext inter_module +) + set(TEST_FILES common.py conftest.py @@ -177,6 +193,7 @@ set(TEST_FILES # Stub reference files test_classes_ext.pyi.ref test_functions_ext.pyi.ref + test_inter_module_1_ext.pyi.ref test_make_iterator_ext.pyi.ref test_ndarray_ext.pyi.ref test_jax_ext.pyi.ref diff --git a/tests/test_inter_module_1_ext.pyi.ref b/tests/test_inter_module_1_ext.pyi.ref new file mode 100644 index 00000000..bb2e86b4 --- /dev/null +++ b/tests/test_inter_module_1_ext.pyi.ref @@ -0,0 +1,3 @@ + + +def create_shared() -> "Shared": ...