-
Notifications
You must be signed in to change notification settings - Fork 63
Dynamic libraries #1083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
No magical handling right now. The relevant PR is #1009, but the various Also the online documentation about it is here if you have any notes/improvements for it. |
Hmm, I am trying to understand. If a cmake project references dynamic libraries that is built with cmake itself, will the wheel folder contain those file? Or I would need to make a command to copy paste the libs next to nanobind built file? Is there any simple example? |
TLDR: It is a bit convoluted. First regarding what do the wheel repairs do, they detect what libraries are dependent on one another using the default library loading methods (RPATH, dlls in the same folder, system libraries, etc.), and then they copy the dependent libraries over (also rename the libraries to a random string) and patch in the dependencies so that they are preferred and loaded when requested (adding RPATH or calling The issue with nanobind (and any other multi-library builds) is that after the installation, the dependency information is gone (RPATH is stripped, and dlls are being dlls), so the wheel repair tools do not have the knowledge of what each library is being linked to. For system libraries it can work more because those would be in the default library locations and are automatically picked up. On our side though, we know during the build which library is dependent on which during the CMake build, so we can more reliably do the correction, which is what's being done in #1009.
I don't have one at hand, but a simple 2 library hello-world project should show the issue. But you want an example of it working or not working? |
Yes just a simple hello world example that would work when I build using I always start from this: But never tried with dynamic libraries. |
This is not as simple as I thought. Just linking a static library from externally downloaded project using SuperBuild pattern does not by default. You need to copy static libraries from externally downloaded folder to the build folder. This is a simple example for static library: cmake_minimum_required(VERSION 3.15)
project(compas_occt LANGUAGES CXX)
# Build configuration
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(ENABLE_PRECOMPILED_HEADERS "Enable precompiled headers for the build" ON)
# ======================================================================
# External dependencies section
# ======================================================================
include(ExternalProject)
# Define paths for external libraries
set(EXTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external")
# ---------------------- Eigen (header-only library) ----------------------
set(EIGEN_INCLUDE_DIR "${EXTERNAL_DIR}/eigen")
set(EIGEN_DOWNLOAD_NEEDED FALSE)
if(NOT EXISTS "${EIGEN_INCLUDE_DIR}")
message(STATUS "Eigen headers not found. Will download.")
set(EIGEN_DOWNLOAD_NEEDED TRUE)
else()
message(STATUS "Using existing Eigen headers at: ${EIGEN_INCLUDE_DIR}")
endif()
# -------------------- Template static library --------------------------
set(TEMPLATE_LIB_DIR "${EXTERNAL_DIR}/template_cpp_static_library")
# Use binary directory instead of source directory to avoid conflicts
set(TEMPLATE_LIB_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/template_static_lib_build")
set(TEMPLATE_LIB_INCLUDE_DIR "${TEMPLATE_LIB_DIR}/src")
set(TEMPLATE_LIB_LIBRARY "${TEMPLATE_LIB_BUILD_DIR}/libcpp_static_lib.a")
set(TEMPLATE_LIB_DOWNLOAD_NEEDED FALSE)
if(NOT EXISTS "${TEMPLATE_LIB_LIBRARY}")
message(STATUS "Template static library not found. Will download and build.")
set(TEMPLATE_LIB_DOWNLOAD_NEEDED TRUE)
else()
message(STATUS "Using existing template static library at: ${TEMPLATE_LIB_LIBRARY}")
endif()
# Create external downloads target
add_custom_target(external_downloads ALL)
# -------------------- Download and build external dependencies --------------------
# Download Eigen if needed
if(EIGEN_DOWNLOAD_NEEDED)
message(STATUS "Downloading Eigen...")
ExternalProject_Add(
eigen_download
URL https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip
SOURCE_DIR "${EIGEN_INCLUDE_DIR}"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD ON
UPDATE_COMMAND ""
PATCH_COMMAND ""
TLS_VERIFY ON
)
add_dependencies(external_downloads eigen_download)
endif()
# Don't clean build directory anymore, as we want to reuse if it exists
# Create/download the template static library only if needed
if(TEMPLATE_LIB_DOWNLOAD_NEEDED)
message(STATUS "Setting up template static library build...")
ExternalProject_Add(
template_cpp_static_library
GIT_REPOSITORY https://github.com/petrasvestartas/template_cpp_static_library.git
GIT_TAG main
PREFIX "${CMAKE_CURRENT_BINARY_DIR}/prefix"
SOURCE_DIR "${TEMPLATE_LIB_DIR}"
BINARY_DIR "${TEMPLATE_LIB_BUILD_DIR}"
CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release
BUILD_BYPRODUCTS "${TEMPLATE_LIB_LIBRARY}"
LOG_DOWNLOAD ON
LOG_CONFIGURE ON
LOG_BUILD ON
UPDATE_COMMAND ""
INSTALL_COMMAND ""
UPDATE_DISCONNECTED ON
)
add_dependencies(external_downloads template_cpp_static_library)
else()
# Create a dummy target to maintain the dependency chain when the library already exists
add_custom_target(template_cpp_static_library
COMMAND ${CMAKE_COMMAND} -E echo "Using existing static library at ${TEMPLATE_LIB_LIBRARY}"
)
add_dependencies(external_downloads template_cpp_static_library)
endif()
# Create an imported target for the static library
add_library(cpp_static_lib STATIC IMPORTED GLOBAL)
set_property(TARGET cpp_static_lib PROPERTY IMPORTED_LOCATION "${TEMPLATE_LIB_LIBRARY}")
add_dependencies(cpp_static_lib template_cpp_static_library)
# Find Python and nanobind
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module Development.SABIModule)
find_package(nanobind CONFIG REQUIRED)
find_package(Threads REQUIRED)
# Add include directories
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/src
${EIGEN_INCLUDE_DIR}
${TEMPLATE_LIB_INCLUDE_DIR}
)
# Define a function to add a nanobind module with common settings
function(add_nanobind_extension name source)
nanobind_add_module(
${name}
STABLE_ABI
NB_STATIC
${source}
)
# Apply precompiled headers
target_precompile_headers(${name} PRIVATE src/compas.h)
# Include directories
target_include_directories(${name} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${EIGEN_INCLUDE_DIR}
${TEMPLATE_LIB_INCLUDE_DIR}
${nanobind_INCLUDE_DIRS}
)
# Make sure the module depends on the external library being built
add_dependencies(${name} template_cpp_static_library)
# Link directly to the static library file (not using an imported target)
target_link_libraries(${name} PRIVATE "${TEMPLATE_LIB_LIBRARY}")
# Install the module
install(TARGETS ${name} LIBRARY DESTINATION compas_occt)
endfunction()
# Create individual extension modules for each C++ file
# Copy this line with new file name and module name
add_nanobind_extension(_primitives src/primitives.cpp)
message(STATUS "============= Build Configuration =============")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C++ Standard: C++${CMAKE_CXX_STANDARD}")
message(STATUS "Eigen Include Dir: ${EIGEN_INCLUDE_DIR}")
message(STATUS "Template Static Library: ${TEMPLATE_LIB_LIBRARY}")
message(STATUS "Template Include Dir: ${TEMPLATE_LIB_INCLUDE_DIR}")
message(STATUS "=======================================") |
Another common issue is that most projects with 3rd party libraries requires first to build them and only then build python wrapper. I was doing before only via header only libraries. For more complex projects single command: I thought I could handle this by How normally this can be handled? |
For this specific case i found a solution, I built OpenCascade, these monster 20 minute build libraries takes planning in advance to understand how to build them. Here is an example of static libraries that worked: cmake_minimum_required(VERSION 3.15)
project(compas_occt LANGUAGES CXX)
# Build configuration
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set(CMAKE_CXX_STANDARD 20 CACHE STRING "C++ standard to use for the build." FORCE)
set(CMAKE_CXX_STANDARD_REQUIRED ON CACHE BOOL "Require the specified C++ standard." FORCE)
set(CMAKE_CXX_EXTENSIONS OFF CACHE BOOL "Disable compiler-specific extensions." FORCE)
# For Python compatibility, ensure ABI compatibility with libstdc++
if(CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_USE_CXX11_ABI=0")
endif()
include(ExternalProject)
# External dependencies will be placed in the build directory to keep the src tree clean
set(EXTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external")
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# -------------------- Eigen header-only library --------------------------
# Eigen is a header-only library, so we only need to download it once
set(EIGEN_INCLUDE_DIR "${EXTERNAL_DIR}/eigen")
set(EIGEN_DOWNLOAD_NEEDED FALSE)
# Check if Eigen folder exists
if (NOT EXISTS "${EIGEN_INCLUDE_DIR}/Eigen/Core")
message(STATUS "Eigen headers not found. Will download.")
set(EIGEN_DOWNLOAD_NEEDED TRUE)
else()
message(STATUS "Using existing Eigen installation at: ${EIGEN_INCLUDE_DIR}")
endif()
# -------------------- OCCT library --------------------------
set(OCCT_DIR "${EXTERNAL_DIR}/occt")
set(OCCT_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/occt_build")
set(OCCT_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/occt_install")
set(OCCT_INCLUDE_DIR "${OCCT_INSTALL_DIR}/include/opencascade")
# Define the key OCCT library we'll use to check if installation exists
set(OCCT_KERNEL_LIBRARY "${OCCT_INSTALL_DIR}/lib/libTKernel")
set(OCCT_DOWNLOAD_NEEDED FALSE)
# Check if OCCT kernel library exists as indicator for complete installation
if(NOT EXISTS "${OCCT_KERNEL_LIBRARY}")
message(STATUS "OCCT library not found. Will download and build.")
set(OCCT_DOWNLOAD_NEEDED TRUE)
else()
message(STATUS "Using existing OCCT installation at: ${OCCT_INSTALL_DIR}")
endif()
# Define OCCT library paths
set(OCCT_LIBRARY_KERNEL "${OCCT_INSTALL_DIR}/lib/libTKernel")
set(OCCT_LIBRARY_MATH "${OCCT_INSTALL_DIR}/lib/libTKMath")
set(OCCT_LIBRARY_G2D "${OCCT_INSTALL_DIR}/lib/libTKG2d")
set(OCCT_LIBRARY_G3D "${OCCT_INSTALL_DIR}/lib/libTKG3d")
set(OCCT_LIBRARY_GEOMBASE "${OCCT_INSTALL_DIR}/lib/libTKGeomBase")
set(OCCT_LIBRARY_BREP "${OCCT_INSTALL_DIR}/lib/libTKBRep")
set(OCCT_LIBRARY_GEOMALGO "${OCCT_INSTALL_DIR}/lib/libTKGeomAlgo")
set(OCCT_LIBRARY_TOPALGO "${OCCT_INSTALL_DIR}/lib/libTKTopAlgo")
set(OCCT_LIBRARY_SHHEALING "${OCCT_INSTALL_DIR}/lib/libTKShHealing")
set(OCCT_LIBRARY_MESH "${OCCT_INSTALL_DIR}/lib/libTKMesh")
set(OCCT_LIBRARY_BO "${OCCT_INSTALL_DIR}/lib/libTKBO")
set(OCCT_LIBRARY_PRIM "${OCCT_INSTALL_DIR}/lib/libTKPrim")
set(OCCT_LIBRARY_FEAT "${OCCT_INSTALL_DIR}/lib/libTKFeat")
set(OCCT_LIBRARY_OFFSET "${OCCT_INSTALL_DIR}/lib/libTKOffset")
set(OCCT_LIBRARY_FILLET "${OCCT_INSTALL_DIR}/lib/libTKFillet")
set(OCCT_LIBRARY_HLR "${OCCT_INSTALL_DIR}/lib/libTKHLR")
set(OCCT_LIBRARY_BOOL "${OCCT_INSTALL_DIR}/lib/libTKBool")
# Create a main target to manage all external dependencies
add_custom_target(external_downloads ALL)
# Create a target to ensure OCCT libraries are ready
add_custom_target(occt_libraries_built
COMMENT "Ensuring OCCT libraries are ready"
)
# -------------------- Download and build external dependencies --------------------
# 1. Download Eigen if needed
if(EIGEN_DOWNLOAD_NEEDED)
message(STATUS "Downloading Eigen...")
ExternalProject_Add(
eigen_download
URL https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip
SOURCE_DIR "${EIGEN_INCLUDE_DIR}"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD ON
UPDATE_COMMAND ""
PATCH_COMMAND ""
TLS_VERIFY ON
)
add_dependencies(external_downloads eigen_download)
endif()
# 2. OCCT setup
if(OCCT_DOWNLOAD_NEEDED)
message(STATUS "Setting up OCCT build...")
ExternalProject_Add(
occt_external
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V7_9_0.zip
PREFIX "${CMAKE_CURRENT_BINARY_DIR}/occt_prefix"
SOURCE_DIR "${OCCT_DIR}"
BINARY_DIR "${OCCT_BUILD_DIR}"
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCCT_INSTALL_DIR}
-DCMAKE_BUILD_TYPE=Release
-DBUILD_LIBRARY_TYPE=Static
-DUSE_TK=OFF
-DUSE_VTK=OFF
-DUSE_FREETYPE=OFF
-DBUILD_SAMPLES_MFC=OFF
-DBUILD_SAMPLES_QT=OFF
-DBUILD_Inspector=OFF
-DINSTALL_SAMPLES=OFF
-DBUILD_USE_PCH=OFF
-DBUILD_OPT_PROFILE=Production
-DBUILD_MODULE_ApplicationFramework=OFF
-DBUILD_MODULE_DataExchange=OFF
-DBUILD_MODULE_Draw=OFF
-DBUILD_MODULE_VisualizationTest=OFF
-DBUILD_MODULE_Visualization=OFF
-DUSE_OPENGL=OFF
# Limit to 2 parallel jobs to avoid memory issues
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -j 2
INSTALL_COMMAND ${CMAKE_COMMAND} --install . --config Release
BUILD_BYPRODUCTS
"${OCCT_LIBRARY_KERNEL}"
"${OCCT_LIBRARY_MATH}"
"${OCCT_LIBRARY_G2D}"
"${OCCT_LIBRARY_G3D}"
"${OCCT_LIBRARY_GEOMBASE}"
"${OCCT_LIBRARY_BREP}"
"${OCCT_LIBRARY_GEOMALGO}"
"${OCCT_LIBRARY_TOPALGO}"
"${OCCT_LIBRARY_SHHEALING}"
"${OCCT_LIBRARY_MESH}"
"${OCCT_LIBRARY_BO}"
"${OCCT_LIBRARY_PRIM}"
"${OCCT_LIBRARY_FEAT}"
"${OCCT_LIBRARY_OFFSET}"
"${OCCT_LIBRARY_FILLET}"
"${OCCT_LIBRARY_HLR}"
"${OCCT_LIBRARY_BOOL}"
LOG_DOWNLOAD ON
LOG_CONFIGURE ON
LOG_BUILD ON
LOG_INSTALL ON
UPDATE_COMMAND ""
UPDATE_DISCONNECTED ON
)
add_dependencies(external_downloads occt_external)
add_dependencies(occt_libraries_built occt_external)
else()
# Create a dummy target when OCCT library exists
add_custom_target(occt_external
COMMAND ${CMAKE_COMMAND} -E echo "Using existing OCCT installation at ${OCCT_INSTALL_DIR}"
)
add_dependencies(external_downloads occt_external)
add_dependencies(occt_libraries_built occt_external)
endif()
# Create imported target for OCCT library
add_library(occt_lib STATIC IMPORTED GLOBAL)
set_property(TARGET occt_lib PROPERTY IMPORTED_LOCATION "${OCCT_KERNEL_LIBRARY}")
add_dependencies(occt_lib occt_external)
# Find Python and nanobind
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module Development.SABIModule)
find_package(nanobind CONFIG REQUIRED)
find_package(Threads REQUIRED)
# Define a function to add a nanobind module with common settings
function(add_nanobind_extension name source)
nanobind_add_module(
${name}
STABLE_ABI
NB_STATIC
${source}
)
# Apply precompiled headers
target_precompile_headers(${name} PRIVATE src/compas.h)
# Include directories
target_include_directories(${name} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${EIGEN_INCLUDE_DIR}
${OCCT_INCLUDE_DIR}
${nanobind_INCLUDE_DIRS}
)
# Create proper dependency chain
add_dependencies(${name} occt_libraries_built)
# Important compiler flags for OCCT
target_compile_definitions(${name} PRIVATE
HAVE_OPENCASCADE
OCCT_NO_PLUGINS
)
# For GCC/Linux, start the group to handle circular dependencies
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_options(${name} PRIVATE "-Wl,--no-as-needed" "-Wl,--start-group")
endif()
# Link with all OCCT libraries in the proper order
target_link_libraries(${name} PRIVATE
# Boolean operations and complex shape handling
"${OCCT_LIBRARY_BOOL}"
"${OCCT_LIBRARY_FILLET}"
"${OCCT_LIBRARY_OFFSET}"
"${OCCT_LIBRARY_FEAT}"
"${OCCT_LIBRARY_PRIM}"
"${OCCT_LIBRARY_BO}"
"${OCCT_LIBRARY_MESH}"
"${OCCT_LIBRARY_HLR}"
"${OCCT_LIBRARY_SHHEALING}"
# Mid-level algorithms and topology
"${OCCT_LIBRARY_TOPALGO}"
"${OCCT_LIBRARY_GEOMALGO}"
# Geometry and representations
"${OCCT_LIBRARY_BREP}"
"${OCCT_LIBRARY_GEOMBASE}"
"${OCCT_LIBRARY_G3D}"
"${OCCT_LIBRARY_G2D}"
# Core libraries
"${OCCT_LIBRARY_MATH}"
"${OCCT_LIBRARY_KERNEL}"
)
# For GCC/Linux, end the group to handle circular dependencies
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_options(${name} PRIVATE "-Wl,--end-group")
endif()
# Install the module
install(TARGETS ${name} LIBRARY DESTINATION compas_occt)
endfunction()
# Create individual extension modules for each C++ file
add_nanobind_extension(_primitives src/primitives.cpp)
# Display build configuration information
message(STATUS "============= Build Configuration =============")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C++ Standard: C++${CMAKE_CXX_STANDARD}")
message(STATUS "Eigen Include Dir: ${EIGEN_INCLUDE_DIR}")
message(STATUS "OCCT Install Dir: ${OCCT_INSTALL_DIR}")
message(STATUS "=======================================")
|
For such big libraries it takes quite a lot of time to build the wheels:
It ends for at least 4 hours of building. Thank you that we have scikit-build-core so that it can be done in github CI. |
Sorry for getting late to you on this issue. Please avoid the Regarding dynamic libraries, I believe we have to split up the issue in:
|
This is the main question:
Short answer is yes, cmake must a) download b) reference include files c) build static or dynamic libraries d) reference them to nanobind module. Does For ubuntu and windows I managed to build and reference correctly the opencascade library. But for mac I have continuous problems since mac has cross platform arm64/x86: https://github.com/petrasvestartas/compas_occt/actions/runs/15107673339 . I almost eager to start from scratch because the CMakeLists became really long and convoluted... But I do not know what is the right way, since it must work in steps: a) download 3rd parties b and c is almost always causing problems. |
Yes, and in a more efficient way. Particularly when you have it combined with
|
I am gradually rewriting with fetchcontent. One problematic point, related to cibuildwheels and scikit-build-core is that mac arm64 and intel version are built in one cibuildworkflow both locally and on github action. Is there any way to split arm64 and x86 into two separate workflows? pyproject.toml [build-system]
requires = ["scikit-build-core >=0.10", "nanobind >=1.3.2"]
build-backend = "scikit_build_core.build"
...
# ============================================================================
# scikit-build
# ============================================================================
[tool.scikit-build]
minimum-version = "build-system.requires"
build-dir = "build/{wheel_tag}"
wheel.py-api = "cp312" # Build all Python currently supported versions.
cmake.version = ">=3.15"
cmake.build-type = "Release"
[tool.scikit-build.metadata.version]
provider = "scikit_build_core.metadata.regex"
input = "src/compas_cgal/__init__.py"
[tool.scikit-build.cmake.define]
CMAKE_POLICY_DEFAULT_CMP0135 = "NEW"
# ============================================================================
# cibuildwheel
# ============================================================================
[tool.cibuildwheel]
# build = ["cp38-*", "cp39-*", "cp31?-*"] # Build for specific Python versions.
build-verbosity = 3
test-requires = ["numpy", "compas", "pytest", "build"]
test-command = "pip install numpy compas && pip list && pytest {project}/tests"
build-frontend = "pip"
manylinux-x86_64-image = "manylinux2014"
skip = ["*_i686", "*-musllinux_*", "*-win32", "pp*"]
macos.environment.MACOSX_DEPLOYMENT_TARGET="11.00"
macos.archs = ["x86_64", "arm64"]
|
Yes, just drop the You might want to run manually |
This helped me so much, I was trying to build my binding for 2 days, and this flag caused so many problems. |
Happy it helped. Feel free to come again if you have other issues |
Uh oh!
There was an error while loading. Please reload this page.
Hi,
I would like to ask how scikit-build-core and nanobind handles dynamic libraries in cmake?
I have a 3rd party library that have dynamic libraries. Do you need to copy .dll/.dylib to wheel directory or it is managed automatically?
The text was updated successfully, but these errors were encountered: