diff --git a/CMakeLists.txt b/CMakeLists.txt index 47502c232242642843f17f468dbe8eadb1f2ed37..ec07537df35c46518a3e9899c3230e4709d6e1d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,14 @@ else() endif() endif() +function(set_standard_output_directory target) + set_target_properties( + ${target} + PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) +endfunction() + # Disable -Werror on Unix for now. set(CXX_DISABLE_WERROR True) set(CMAKE_VERBOSE_MAKEFILE True) @@ -328,6 +336,7 @@ set(${PROJECT_NAME}_SOURCES add_library(${PROJECT_NAME} SHARED ${${PROJECT_NAME}_SOURCES} ${${PROJECT_NAME}_HEADERS}) +set_standard_output_directory(${PROJECT_NAME}) target_include_directories( ${PROJECT_NAME} SYSTEM PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include> diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 4a9825a98e095c65068930ad6d0a8560d1620631..4781e42f35a6d08e6b0e0f00960c61a162ed42b5 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -67,7 +67,7 @@ if(GENERATE_PYTHON_STUBS) endif(GENERATE_PYTHON_STUBS) # --- INSTALL SCRIPTS -set(PYTHON_FILES __init__.py) +set(PYTHON_FILES __init__.py windows_dll_manager.py) foreach(python ${PYTHON_FILES}) python_build(${PROJECT_NAME} ${python}) diff --git a/python/eigenpy/__init__.py b/python/eigenpy/__init__.py index 503852acdf8d8b7cac73b3ef7b5f32215971b968..7fdeef13b8220a43791f4d638c7100a89ab850b1 100644 --- a/python/eigenpy/__init__.py +++ b/python/eigenpy/__init__.py @@ -2,5 +2,29 @@ # Copyright (c) 2017-2021 CNRS INRIA # -from .eigenpy_pywrap import * # noqa -from .eigenpy_pywrap import __raw_version__, __version__ # noqa +# On Windows, if eigenpy.dll is not in the same directory than +# the .pyd, it will not be loaded. +# We first try to load eigenpy, then, if it fail and we are on Windows: +# 1. We add all paths inside eigenpy_WINDOWS_DLL_PATH to DllDirectory +# 2. If EIGENPY_WINDOWS_DLL_PATH we add the relative path from the +# package directory to the bin directory to DllDirectory +# This solution is inspired from: +# - https://github.com/PixarAnimationStudios/OpenUSD/pull/1511/files +# - https://stackoverflow.com/questions/65334494/python-c-extension-packaging-dll-along-with-pyd +# More resources on https://github.com/diffpy/pyobjcryst/issues/33 +try: + from .eigenpy_pywrap import * # noqa + from .eigenpy_pywrap import __raw_version__, __version__ # noqa +except ImportError: + import platform + + if platform.system() == "Windows": + from .windows_dll_manager import get_dll_paths, build_directory_manager + + with build_directory_manager() as dll_dir_manager: + for p in get_dll_paths(): + dll_dir_manager.add_dll_directory(p) + from .eigenpy_pywrap import * # noqa + from .eigenpy_pywrap import __raw_version__, __version__ # noqa + else: + raise diff --git a/python/eigenpy/windows_dll_manager.py b/python/eigenpy/windows_dll_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..5f6482c06fccad5adc2723c04c3ed3a13417f23c --- /dev/null +++ b/python/eigenpy/windows_dll_manager.py @@ -0,0 +1,62 @@ +import os +import sys +import contextlib + + +def get_dll_paths(): + eigenpy_paths = os.getenv("EIGENPY_WINDOWS_DLL_PATH") + if eigenpy_paths is None: + # From https://peps.python.org/pep-0250/#implementation + # lib/python-version/site-packages/package + RELATIVE_DLL_PATH1 = "..\\..\\..\\..\\bin" + # lib/site-packages/package + RELATIVE_DLL_PATH2 = "..\\..\\..\\bin" + return [ + os.path.join(os.path.dirname(__file__), RELATIVE_DLL_PATH1), + os.path.join(os.path.dirname(__file__), RELATIVE_DLL_PATH2), + ] + else: + return eigenpy_paths.split(os.pathsep) + + +class PathManager(contextlib.AbstractContextManager): + """Restore PATH state after importing Python module""" + + def add_dll_directory(self, dll_dir: str): + os.environ["PATH"] += os.pathsep + dll_dir + + def __enter__(self): + self.old_path = os.environ["PATH"] + return self + + def __exit__(self, *exc_details): + os.environ["PATH"] = self.old_path + + +class DllDirectoryManager(contextlib.AbstractContextManager): + """Restore DllDirectory state after importing Python module""" + + def add_dll_directory(self, dll_dir: str): + # add_dll_directory can fail on relative path and non + # existing path. + # Since we don't know all the fail criterion we just ignore + # thrown exception + try: + self.dll_dirs.append(os.add_dll_directory(dll_dir)) + except OSError: + pass + + def __enter__(self): + self.dll_dirs = [] + return self + + def __exit__(self, *exc_details): + for d in self.dll_dirs: + d.close() + + +def build_directory_manager(): + if sys.version_info >= (3, 8): + return DllDirectoryManager() + else: + return PathManager() diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index f5d573773a6779691fb5518dc9bf83b3c7af383e..09b35a02727f7008b6ca737847e0a537c5d9c4c9 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -10,13 +10,17 @@ macro(ADD_LIB_UNIT_TEST test) else(BUILD_TESTING) add_library(${test} SHARED EXCLUDE_FROM_ALL "${test}.cpp") endif(BUILD_TESTING) + set_standard_output_directory(${test}) target_link_libraries(${test} PUBLIC ${PROJECT_NAME}) set_target_properties(${test} PROPERTIES PREFIX "") set_target_properties(${test} PROPERTIES SUFFIX ${PYTHON_EXT_SUFFIX}) - add_test(NAME ${test} COMMAND ${PYTHON_EXECUTABLE} -c "import ${test}") + add_test( + NAME ${test} + COMMAND ${PYTHON_EXECUTABLE} -c "import ${test}" + WORKING_DIRECTORY $<TARGET_FILE_DIR:${test}>) add_dependencies(build_tests ${test}) if(NOT BUILD_TESTING) @@ -51,6 +55,20 @@ if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98) add_lib_unit_test(std_unique_ptr) endif() +function(add_python_lib_unit_test name source) + add_python_unit_test(${name} ${source} "lib" "bin") +endfunction() + +function(add_python_eigenpy_lib_unit_test name source) + add_python_unit_test(${name} ${source} "lib" "bin" "python") + set_tests_properties(${name} PROPERTIES DEPENDS ${PYWRAP}) +endfunction() + +function(add_python_eigenpy_unit_test name source) + add_python_unit_test(${name} ${source} "python") + set_tests_properties(${name} PROPERTIES DEPENDS ${PYWRAP}) +endfunction() + function(config_test test tagname opttype) set(MODNAME ${test}_${tagname}) set(TEST_TYPE ${opttype}) @@ -64,7 +82,7 @@ function(config_test test tagname opttype) add_test(NAME ${PYTHON_TEST_NAME} COMMAND ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/python/${py_file}") - compute_pythonpath(ENV_VARIABLES "unittest") + compute_pythonpath(ENV_VARIABLES "lib" "bin") set_tests_properties(${PYTHON_TEST_NAME} PROPERTIES ENVIRONMENT "${ENV_VARIABLES}") endfunction() @@ -81,106 +99,87 @@ endif() add_lib_unit_test(bind_virtual_factory) -add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest") +add_python_lib_unit_test("py-matrix" "unittest/python/test_matrix.py") -add_python_unit_test("py-tensor" "unittest/python/test_tensor.py" "unittest") -add_python_unit_test("py-geometry" "unittest/python/test_geometry.py" - "unittest") -add_python_unit_test("py-complex" "unittest/python/test_complex.py" "unittest") -add_python_unit_test("py-return-by-ref" "unittest/python/test_return_by_ref.py" - "unittest") -add_python_unit_test("py-eigen-ref" "unittest/python/test_eigen_ref.py" - "unittest") +add_python_lib_unit_test("py-tensor" "unittest/python/test_tensor.py") +add_python_lib_unit_test("py-geometry" "unittest/python/test_geometry.py") +add_python_lib_unit_test("py-complex" "unittest/python/test_complex.py") +add_python_lib_unit_test("py-return-by-ref" + "unittest/python/test_return_by_ref.py") +add_python_lib_unit_test("py-eigen-ref" "unittest/python/test_eigen_ref.py") if(NOT NUMPY_WITH_BROKEN_UFUNC_SUPPORT) - add_python_unit_test("py-user-type" "unittest/python/test_user_type.py" - "unittest") + add_python_lib_unit_test("py-user-type" "unittest/python/test_user_type.py") endif() -add_python_unit_test("py-dimensions" "unittest/python/test_dimensions.py" - "python;unittest") -set_tests_properties("py-dimensions" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-dimensions" + "unittest/python/test_dimensions.py") -add_python_unit_test("py-version" "unittest/python/test_version.py" - "python;unittest") -set_tests_properties("py-version" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-version" "unittest/python/test_version.py") -add_python_unit_test("py-eigen-solver" "unittest/python/test_eigen_solver.py" - "python;unittest") -set_tests_properties("py-eigen-solver" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-eigen-solver" + "unittest/python/test_eigen_solver.py") -add_python_unit_test( +add_python_eigenpy_lib_unit_test( "py-self-adjoint-eigen-solver" - "unittest/python/test_self_adjoint_eigen_solver.py" "python;unittest") -set_tests_properties("py-self-adjoint-eigen-solver" PROPERTIES DEPENDS - ${PYWRAP}) + "unittest/python/test_self_adjoint_eigen_solver.py") -add_python_unit_test("py-LLT" "unittest/python/test_LLT.py" "python;unittest") -set_tests_properties("py-LLT" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-LLT" "unittest/python/test_LLT.py") -add_python_unit_test("py-LDLT" "unittest/python/test_LDLT.py" "python;unittest") -set_tests_properties("py-LDLT" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-LDLT" "unittest/python/test_LDLT.py") if(NOT WIN32) - add_python_unit_test("py-MINRES" "unittest/python/test_MINRES.py" - "python;unittest") - set_tests_properties("py-MINRES" PROPERTIES DEPENDS ${PYWRAP}) + add_python_eigenpy_lib_unit_test("py-MINRES" "unittest/python/test_MINRES.py") endif(NOT WIN32) -add_python_unit_test("py-std-vector" "unittest/python/test_std_vector.py" - "python;unittest") -set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP}) +add_python_eigenpy_lib_unit_test("py-std-vector" + "unittest/python/test_std_vector.py") -add_python_unit_test("py-std-array" "unittest/python/test_std_array.py" - "unittest") +add_python_lib_unit_test("py-std-array" "unittest/python/test_std_array.py") -add_python_unit_test("py-std-pair" "unittest/python/test_std_pair.py" - "unittest") +add_python_lib_unit_test("py-std-pair" "unittest/python/test_std_pair.py") -add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py" - "unittest") +add_python_lib_unit_test("py-user-struct" "unittest/python/test_user_struct.py") if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98) - add_python_unit_test("py-std-unique-ptr" - "unittest/python/test_std_unique_ptr.py" "unittest") + add_python_lib_unit_test("py-std-unique-ptr" + "unittest/python/test_std_unique_ptr.py") endif() -add_python_unit_test("py-bind-virtual" "unittest/python/test_bind_virtual.py" - "unittest") +add_python_lib_unit_test("py-bind-virtual" + "unittest/python/test_bind_virtual.py") if(BUILD_TESTING_SCIPY) - add_python_unit_test("py-sparse-matrix" - "unittest/python/test_sparse_matrix.py" "unittest") + add_python_lib_unit_test("py-sparse-matrix" + "unittest/python/test_sparse_matrix.py") - add_python_unit_test( + add_python_eigenpy_unit_test( "py-SimplicialLLT" - "unittest/python/decompositions/sparse/test_SimplicialLLT.py" "python") - add_python_unit_test( + "unittest/python/decompositions/sparse/test_SimplicialLLT.py") + add_python_eigenpy_unit_test( "py-SimplicialLDLT" - "unittest/python/decompositions/sparse/test_SimplicialLDLT.py" "python") + "unittest/python/decompositions/sparse/test_SimplicialLDLT.py") if(BUILD_WITH_CHOLMOD_SUPPORT) - - add_python_unit_test( + add_python_eigenpy_unit_test( "py-CholmodSimplicialLLT" "unittest/python/decompositions/sparse/cholmod/test_CholmodSimplicialLLT.py" - "python") + ) - add_python_unit_test( + add_python_eigenpy_unit_test( "py-CholmodSimplicialLDLT" "unittest/python/decompositions/sparse/cholmod/test_CholmodSimplicialLDLT.py" - "python") + ) - add_python_unit_test( + add_python_eigenpy_unit_test( "py-CholmodSupernodalLLT" "unittest/python/decompositions/sparse/cholmod/test_CholmodSupernodalLLT.py" - "python") - + ) endif(BUILD_WITH_CHOLMOD_SUPPORT) if(BUILD_WITH_ACCELERATE_SUPPORT) - add_python_unit_test( + add_python_eigenpy_unit_test( "py-Accelerate" - "unittest/python/decompositions/sparse/test_Accelerate.py" "python") + "unittest/python/decompositions/sparse/test_Accelerate.py") endif(BUILD_WITH_ACCELERATE_SUPPORT) endif()