I have a project, cloudgen, that I would like to add bindings for Python so I can access some of the underlying functions. I have stubbed out the initial work on a branch. Because the main executable is built with cmake, I decided to use scikit-build to manage the build and use pybind11 to deal with the binding (following this example repo).
When I run pip install .
in a virtual environment, everything appears to work as expected. I find the executable is installed to <prefix>/bin
, the library goes into <prefix>/lib
, and the module goes into <prefix>/lib/pythonX.Y/site-packages/cloudgen
. In fact, if I run pip uninstall cloudgen
, all of the correct files are uninstalled. However, my problems arise when I start to test the Python bindings. I find two separate but related problems.
- If I installed into an Anaconda environment, the module is able to resolve the path to the shared library and pass the tests, but the executable does not resolve the path to the library.
- On the other hand, if I installed into a virtual environment using
python -m venv
, both the module and the executable are unable to resolve the path to the shared library.
Searching around, I came across this question which notes I could manipulate LD_LIBRARY_PATH
(or equivalently DYLD_LIBRARY_PATH
on macOS or PATH
on Windows), but that is normally frowned upon. That question references an open issue that refers to including additional build products (which as I said appears to not be my problem) but doesn’t address the library path resolution. I also came across this question asking about distributing the build products using scikit-build and this question using setuptools directly. Neither of the questions or answers address the library path resolution.
My question is: What is the correct way to distribute a package that contains an executable, shared library, and Python binding module and have the path resolution Just Work�
A minimal working example is a bit much, but I created a gist to demonstrate the behavior.
Advertisement
Answer
After a bit more digging (and carefully reading the CMake documentation on RPATH
), the correct answer appears to be explicitly setting RPATH
on installation. The relevant change to the linked gist is to add the following to the CMakeLists.txt after creating the targets (adapted from the linked Wiki):
if (SKBUILD) find_package(PythonExtensions REQUIRED) set(lib_path "${PYTHON_PREFIX}/${CMAKE_INSTALL_LIBDIR}") else() set(lib_path "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") endif() list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${lib_path}" is_system) if ("${is_system}" STREQUAL "-1") set_target_properties(mwe.exe PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE INSTALL_RPATH "${lib_path}") # The following is necessary for installation in a virtual # environment `python -m pip venv env` set_target_properties(_mwe PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE INSTALL_RPATH "${lib_path}") endif()
This does require following the rest of the details about setting the RPATH
such as including (literally from the linked CMake Wiki):
# use, i.e. don't skip the full RPATH for the build tree set(CMAKE_SKIP_BUILD_RPATH FALSE) # when building, don't use the install RPATH already # (but later on when installing) set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") # add the automatically determined parts of the RPATH # which point to directories outside the build tree to the install RPATH set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
earlier in the CMakeLists.txt. The net result is this should disable the dynamic searching of the library path (LD_LIBRARY_PATH
or DYLD_LIBRARY_PATH
as appropriate) for the installed executable and Python module.