Skip to content

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

Closed
petrasvestartas opened this issue May 16, 2025 · 15 comments
Closed

Dynamic libraries #1083

petrasvestartas opened this issue May 16, 2025 · 15 comments

Comments

@petrasvestartas
Copy link

petrasvestartas commented May 16, 2025

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?

@LecrisUT
Copy link
Collaborator

LecrisUT commented May 16, 2025

No magical handling right now. The relevant PR is #1009, but the various auditwheel and such might be able to handle it. It is more of a gray area because the linkage is within the wheel, and the RPATH is not available without manual intervention in those cases.

Also the online documentation about it is here if you have any notes/improvements for it.

@petrasvestartas
Copy link
Author

petrasvestartas commented May 16, 2025

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?
( i am not using any dynamic library that i do not build myself )

Is there any simple example?

@LecrisUT
Copy link
Collaborator

TLDR: auditwheel and co. might do the job for you automagically, if you point LD_LIBRARY_PATH/DYLD_LIBRARY_PATH/PATH appropriately.

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 os.add_dll_directory). As long as the dependencies are able to be resolved, the wheel repair tools will handle the dirty work for you. You can use LD_LIBRARY_PATH/DYLD_LIBRARY_PATH/PATH to help it.

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.

Is there any simple example?

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?

@petrasvestartas
Copy link
Author

petrasvestartas commented May 16, 2025

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 cibuildwheels
It would be amazing:)

I always start from this:
https://github.com/wjakob/nanobind_example

But never tried with dynamic libraries.

@petrasvestartas
Copy link
Author

petrasvestartas commented May 18, 2025

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:
https://github.com/petrasvestartas/compas_occt/blob/f1354ba27c62cedf106a1eec9b3b353f338a97c6/CMakeLists.txt

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 "=======================================")

@petrasvestartas
Copy link
Author

petrasvestartas commented May 18, 2025

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:
pip install --no-build-isolation -ve . or cibuildwheel --output-dir wheelhouse .

I thought I could handle this by externproject setup of cmake but I need to use two step built process.

How normally this can be handled?

@petrasvestartas
Copy link
Author

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 "=======================================")


@petrasvestartas
Copy link
Author

petrasvestartas commented May 18, 2025

For such big libraries it takes quite a lot of time to build the wheels:

  • One compilation takes 20 minutes
  • You need to multiply by python version you want to support e.g. 3.9, 3.10, 3.11, 3.12, so four times
  • you have 3 operating system - 3 times

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.

@LecrisUT
Copy link
Collaborator

Sorry for getting late to you on this issue.

Please avoid the ExternalProject design. Use FetchContent instead, preferably with the find_package support in CMake 3.24. There are so many complications and issues with that, including the excessive rebuilding, which we can discuss in more detail in #1086.

Regarding dynamic libraries, I believe we have to split up the issue in:

  • dynamic libraries of system/non-python libraries
    • This is a case-by-case issue depending on what wheel and build models do you want to support. Will you always bundle the dependency, do you want to support users having the dependency pre-installed, how would the distro packagers deal with it, do you support sdist builds?
  • dynamic libraries in the same build (e.g. nanobind shared library):
    • This may or may not work with cibuildwheel. Try it out on the nanobind_example. If it doesn't work out-of-the-box, then it's something that feat: Integrate wheel repairer #1009 will help.

@petrasvestartas
Copy link
Author

petrasvestartas commented May 19, 2025

@LecrisUT

This is the main question:

Will you always bundle the dependency, do you want to support users having the dependency pre-installed, how would the distro packagers deal with it, do you support sdist builds?

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 FetchContent can do this?

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) build first 3rd parties
c) reference 3rd parties libraries to nanobind target.

b and c is almost always causing problems.

@LecrisUT
Copy link
Collaborator

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 FetchContent can do this?

Yes, and in a more efficient way. Particularly when you have it combined with find_package:

  • For sdist builds and other builds the 3rd-party will be downloaded and build/installed as part of the current project. Make sure you adjust the 3rd-party options accordingly to install/build the appropriate shared/static libraries as you desire. The default scikit-build-core install should be sufficient to not need to alter the 3rd-party install locations if you use wheel.install-dir
  • For cibuildwheel, you can do a pre-build of the 3rd-party and use the find_package to pick those artifacts instead. Then auditwheel or other wheel repairs will take care of including the library inside the wheel.

@petrasvestartas
Copy link
Author

petrasvestartas commented May 19, 2025

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"]

@LecrisUT
Copy link
Collaborator

Is there any way to split arm64 and x86 into two separate workflows?

Yes, just drop the macos.archs. The default right now should be to build only for the native arch, if not, you could add CIBW_ARCHS=native explicitly. See the example for the recommended matrix.

You might want to run manually delocatewheel on top of it if you want unified wheels, but personally I wouldn't bother with that.

@petrasvestartas
Copy link
Author

petrasvestartas commented May 20, 2025

Yes, just drop the macos.archs

This helped me so much, I was trying to build my binding for 2 days, and this flag caused so many problems.
Thank you!

@LecrisUT
Copy link
Collaborator

Happy it helped. Feel free to come again if you have other issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants