################################################################################ # # # This file is part of IfcOpenShell. # # # # IfcOpenShell is free software: you can redistribute it and/or modify # # it under the terms of the Lesser GNU General Public License as published by # # the Free Software Foundation, either version 3.0 of the License, or # # (at your option) any later version. # # # # IfcOpenShell is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # Lesser GNU General Public License for more details. # # # # You should have received a copy of the Lesser GNU General Public License # # along with this program. If not, see . # # # ################################################################################ cmake_minimum_required(VERSION 3.21) if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) project(IfcOpenShellPython LANGUAGES CXX) set(IFCOPENSHELL_IFCWRAP_STANDALONE ON) else() set(IFCOPENSHELL_IFCWRAP_STANDALONE OFF) endif() if(NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() if(CMAKE_CXX_STANDARD LESS 17) message(FATAL_ERROR "C++17 or newer is required.") endif() set(CMAKE_CXX_STANDARD_REQUIRED ON) if(POLICY CMP0148) # 3.27 cmake_policy(SET CMP0148 OLD) endif() if(POLICY CMP0177) # 3.31 cmake_policy(SET CMP0177 OLD) endif() set(_ifcwrap_feature_definitions "") if(IFCOPENSHELL_IFCWRAP_STANDALONE) get_filename_component(_ifcwrap_repo_root "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) list(PREPEND CMAKE_MODULE_PATH "${_ifcwrap_repo_root}/cmake") find_package(IfcOpenShell REQUIRED) set(WITH_OPENCASCADE "${IFCOPENSHELL_WITH_OPENCASCADE}") set(WITH_CGAL "${IFCOPENSHELL_WITH_CGAL}") set(SWIG_DEFINES "") if(IFCOPENSHELL_WITH_OPENCASCADE) list(APPEND SWIG_DEFINES -DIFOPSH_WITH_OPENCASCADE) list(APPEND _ifcwrap_feature_definitions IFOPSH_WITH_OPENCASCADE) endif() if(IFCOPENSHELL_WITH_CGAL) list(APPEND SWIG_DEFINES -DIFOPSH_WITH_CGAL) list(APPEND _ifcwrap_feature_definitions IFOPSH_WITH_CGAL) endif() if(IFCOPENSHELL_IFCXML) list(APPEND SWIG_DEFINES -DWITH_IFCXML) list(APPEND _ifcwrap_feature_definitions WITH_IFCXML) endif() if(IFCOPENSHELL_WITH_ROCKSDB) list(APPEND SWIG_DEFINES -DIFOPSH_WITH_ROCKSDB) list(APPEND _ifcwrap_feature_definitions IFOPSH_WITH_ROCKSDB) endif() endif() find_package(SWIG) if(NOT SWIG_FOUND) message( FATAL_ERROR "BUILD_IFCPYTHON enabled, but unable to find SWIG. Disable BUILD_IFCPYTHON or fix SWIG paths to proceed. " "Likely SWIG_EXECUTABLE is missing (current value - '${SWIG_EXECUTABLE}')." ) ENDIF() include(GNUInstallDirs) INCLUDE(${SWIG_USE_FILE}) IF(NOT "${PYTHON_INCLUDE_DIR}" STREQUAL "") # Hint for FindPython where to find Python_INCLUDE_DIRS. set(Python_INCLUDE_DIR "${PYTHON_INCLUDE_DIR}") MESSAGE(STATUS "Looking for Python header files in: ${Python_INCLUDE_DIR}") ENDIF() IF(NOT "${PYTHON_LIBRARY}" STREQUAL "") # Hint for FindPython where to find Python_LIBRARIES. set(Python_LIBRARY "${PYTHON_LIBRARY}") MESSAGE(STATUS "Looking for Python library file in: ${Python_LIBRARY}") ENDIF() # Development.Module = headers on Unix, headers+libraries on Windows. # Required variables: # - Windows - Python_INCLUDE_DIR, Python_LIBRARY (MSVC doesn't allow undefined symbols at link time) # - Unix - Python_INCLUDE_DIR # Unfortunately all paths must be always provided explicitly, not by prefixing PATH. # Otherwise FindPython will break on newer Python versions (list of supported versions is hardcoded). find_package(Python COMPONENTS Development.Module) IF(NOT Python_Development.Module_FOUND) MESSAGE(FATAL_ERROR "BUILD_IFCPYTHON enabled, but unable to find Python lib or header. Disable BUILD_IFCPYTHON or fix Python paths to proceed.") ENDIF() # Ensure version is saved here, from wasm libraries, # not from Python interpreter that might be unrelated to pyodide Python version. set(_python_libs_version "${Python_VERSION_MAJOR}${Python_VERSION_MINOR}") INCLUDE_DIRECTORIES(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) #`%feature("python:abc")` is a feature we use in swig template. # Nn swig < 4.1 it works only if `-py3` is set explictily, # in swig 4.1+ `-py3` is no-op. # E.g. Ubuntu 22.04 is still using swig 4.0.2. if(SWIG_VERSION VERSION_LESS "4.1") list(APPEND SWIG_DEFINES -py3) endif() SET(CMAKE_SWIG_FLAGS ${SWIG_DEFINES}) if(WITH_CGAL) if(IFCOPENSHELL_IFCWRAP_STANDALONE) if(TARGET IfcOpenShell::svgfill) set(LIBSVGFILL IfcOpenShell::svgfill) else() message(FATAL_ERROR "IfcOpenShell was built with CGAL, but the exported svgfill target was not found.") endif() else() set(LIBSVGFILL svgfill) endif() endif() set_source_files_properties(IfcPython.i PROPERTIES CPLUSPLUS ON) # Rebuild on changes in other .i files. SET_PROPERTY( SOURCE IfcPython.i PROPERTY DEPENDS IfcGeomWrapper.i IfcParseWrapper.i utils/type_conversion.i utils/typemaps_in.i utils/typemaps_out.i ) # On Windows there is '_d' prefix for debug builds - e.g. `_d.cp311-win_amd64.pyd`. # In 4.2+ it's done automatically when discovering `Development.Module`. if(WIN32 AND CMAKE_VERSION VERSION_LESS "4.2") set(Python_DEBUG_POSTFIX "_d") endif() # Workaround for most likely missing debug Python libraries on Windows. # Even if user has them, it's unlikely that they want to use them for building IfcPython # since it makes extension incompatiblew with standard Python # and requires all other extensions to be built in debug mode as well. # But debug mode is still might be needed - e.g. Blender's Python is using it, if you're running debug build. if(NOT USE_DEBUG_PYTHON) set(Python_DEBUG_POSTFIX "") add_compile_definitions(SWIG_PYTHON_INTERPRETER_NO_DEBUG) if(Python_LIBRARY_DEBUG) # If debug libraries were found, then override them with release ones. get_target_property(IMPORTED_IMPLIB Python::Module IMPORTED_IMPLIB_RELEASE) get_target_property(IMPORTED_LOCATION Python::Module IMPORTED_LOCATION_RELEASE) set_target_properties( Python::Module PROPERTIES IMPORTED_IMPLIB_DEBUG "${IMPORTED_IMPLIB}" IMPORTED_LOCATION_DEBUG "${IMPORTED_LOCATION}" ) endif() endif() if (CMAKE_VERSION VERSION_GREATER_EQUAL "4.2") # `DEBUG_POSTFIX` argument was added in 4.2. swig_add_library(ifcopenshell_wrapper LANGUAGE python SOURCES IfcPython.i DEBUG_POSTFIX "${Python_DEBUG_POSTFIX}") else() # If we won't override -interface name, # swig py wrapper will try to import `_ifcopenshell_wrapper_d` module and would fail. set(SWIG_MODULE_ifcopenshell_wrapper_EXTRA_FLAGS "-interface" "_ifcopenshell_wrapper") swig_add_library(ifcopenshell_wrapper LANGUAGE python SOURCES IfcPython.i) set_target_properties( ifcopenshell_wrapper PROPERTIES DEBUG_POSTFIX "${Python_DEBUG_POSTFIX}" ) endif() target_link_libraries(ifcopenshell_wrapper PRIVATE Python::Module) if(WITH_CGAL) target_sources(ifcopenshell_wrapper PRIVATE ../svgfill/src/linework_processing_facade.cpp) endif() if(WITH_ROCKSDB) target_link_libraries(ifcopenshell_wrapper PRIVATE document_serializer_rdb) endif() if(_ifcwrap_feature_definitions) target_compile_definitions(ifcopenshell_wrapper PRIVATE ${_ifcwrap_feature_definitions}) endif() if(IFCOPENSHELL_IFCWRAP_STANDALONE) set(_ifcwrap_ifcopenshell_libraries ${IFCOPENSHELL_LIBRARIES}) set(_ifcwrap_geometry_libraries ${IFCOPENSHELL_GEOMETRY_LIBRARIES}) set(_ifcwrap_cgal_libraries "") set(_ifcwrap_swig_depends "") else() set(_ifcwrap_ifcopenshell_libraries ${IFCOPENSHELL_LIBRARIES}) set(_ifcwrap_geometry_libraries IfcGeom) set(_ifcwrap_cgal_libraries ${CGAL_LIBRARIES}) set(_ifcwrap_swig_depends ${IFCOPENSHELL_LIBRARIES}) endif() set_property(TARGET ifcopenshell_wrapper PROPERTY SWIG_DEPENDS ${_ifcwrap_swig_depends}) if(WASM_BUILD) # SIDE_MODULE=1 - add to .so all symbols from linked archives (default used by pyodide). # Since currently libIfcGeom.a seems to be linked twice it results in duplicated symbols and compilation errors. # Possibly in the future we can clean up linked libs and try `=1`. # SIDE_MODULE=2 - add to .so only explicitly exported functions (`-s EXPORTED_FUNCTIONS). # Have to provide it here to override possible `=1` coming from pyodide. if(CMAKE_GENERATOR MATCHES "Makefile" AND CMAKE_VERSION VERSION_LESS "4.2") # Accomodate bug in cmake makefile generator (fixed in 4.2+). # https://gitlab.kitware.com/cmake/cmake/-/issues/27326 string( REPLACE "SIDE_MODULE=1" "SIDE_MODULE=2" CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS}" ) endif() target_link_options( ifcopenshell_wrapper PRIVATE "SHELL:-s EXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper -s SIDE_MODULE=2" -O1 ) endif() target_link_libraries(ifcopenshell_wrapper PRIVATE ${_ifcwrap_geometry_libraries} ${_ifcwrap_ifcopenshell_libraries} ${Boost_LIBRARIES} ) if(schema_libraries OR kernel_libraries OR tree_libraries OR mapping_libraries OR geometry_serializer_libraries OR document_serializer_libraries OR linework_processing_libraries) add_dependencies(ifcopenshell_wrapper ${schema_libraries} ${kernel_libraries} ${tree_libraries} ${mapping_libraries} ${geometry_serializer_libraries} ${document_serializer_libraries} ${linework_processing_libraries}) endif() if((NOT WIN32) AND NOT WASM_BUILD AND NOT CREATE_BUNDLE AND NOT CMAKE_INSTALL_RPATH AND COMMAND SET_INSTALL_RPATHS) if(IFCOPENSHELL_IFCWRAP_STANDALONE) set(_ifcwrap_ifcopenshell_library_dir "${IFCOPENSHELL_LIBRARY_DIR}") else() set(_ifcwrap_ifcopenshell_library_dir "${CMAKE_INSTALL_FULL_LIBDIR}") endif() # The Python package and IfcOpenShell libraries can have unrelated install # prefixes. This is notably the case for standalone ifcwrap builds against # an installed CMake package and GitHub Actions' hosted Python. SET_INSTALL_RPATHS(ifcopenshell_wrapper "${_ifcwrap_ifcopenshell_library_dir};${IFCDIRS};${OCC_LIBRARY_DIR}") elseif((NOT WIN32) AND BUILD_SHARED_LIBS AND COMMAND SET_INSTALL_RPATHS) SET_INSTALL_RPATHS(ifcopenshell_wrapper "${IFCDIRS};${OCC_LIBRARY_DIR}") endif() # Try to find the Python interpreter to get the site-packages # directory in which the wrapper can be installed. if(NOT ${PYTHON_EXECUTABLE} STREQUAL "") set(Python_EXECUTABLE "${PYTHON_EXECUTABLE}") MESSAGE(STATUS "Looking for Python interpreter in: ${PYTHON_EXECUTABLE}") endif() FIND_PACKAGE(Python COMPONENTS Interpreter) IF(Python_Interpreter_FOUND OR PYTHON_MODULE_INSTALL_DIR) # Python_SOABI example value is 'cp311-win_amd64'. if(WASM_BUILD) # For WASM we usually don't provide interpreter, # so `find_package` couldn't find `Python_SOABI`. # So we just hardcode it. # .cpython-313-wasm32-emscripten.so set(PYTHON_EXTENSION_SUFFIX ".cpython-${_python_libs_version}-wasm32-emscripten.so") elseif(WIN32) set(PYTHON_EXTENSION_SUFFIX ".${Python_SOABI}.pyd") else() set(PYTHON_EXTENSION_SUFFIX ".${Python_SOABI}.so") endif() set_target_properties( ifcopenshell_wrapper PROPERTIES SUFFIX ${PYTHON_EXTENSION_SUFFIX} ) if (PYTHON_MODULE_INSTALL_DIR) set(python_package_dir "${PYTHON_MODULE_INSTALL_DIR}") else() IF (USERSPACE_PYTHON_PREFIX) EXECUTE_PROCESS( COMMAND ${PYTHON_EXECUTABLE} -c "import sys; import site; sys.stdout.write(site.USER_SITE)" OUTPUT_VARIABLE python_package_dir ) ELSE () set(python_package_dir "${Python_SITEARCH}") ENDIF() if (BUILD_PACKAGE) set(python_package_dir ${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_MAJOR}/dist-packages/) endif() endif() IF("${python_package_dir}" STREQUAL "") MESSAGE(WARNING "Unable to locate Python site-package directory, unable to install the Python wrapper") ELSE() message(STATUS "Python wrapper will be installed to '${python_package_dir}'.") file(GLOB_RECURSE sourcefiles "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*") # Exclude compiled extensions - they're part of dev environment and shouldn't leak into installation. list(FILTER sourcefiles EXCLUDE REGEX "\\.(so|pyd|dylib)$") foreach(file ${sourcefiles}) file(RELATIVE_PATH relative "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/" "${file}") get_filename_component(dir "${relative}" DIRECTORY) if(NOT IS_DIRECTORY "${file}") install(FILES "${file}" DESTINATION "${python_package_dir}/ifcopenshell/${dir}") endif() endforeach() install( FILES "${CMAKE_CURRENT_BINARY_DIR}/ifcopenshell_wrapper.py" DESTINATION "${python_package_dir}/ifcopenshell" ) INSTALL(TARGETS ifcopenshell_wrapper DESTINATION "${python_package_dir}/ifcopenshell") if (CREATE_BUNDLE) set(_ifcopenshell_python_runtime_targets IfcGeom IfcParse ${schema_libraries} ${kernel_libraries} ${tree_libraries} ${mapping_libraries} geometry_serializer ${geometry_serializer_libraries} ${document_serializer_libraries} ${linework_processing_libraries} plugin ) list(REMOVE_DUPLICATES _ifcopenshell_python_runtime_targets) if(_ifcopenshell_python_runtime_targets) INSTALL(TARGETS ${_ifcopenshell_python_runtime_targets} LIBRARY DESTINATION "${python_package_dir}/ifcopenshell" RUNTIME DESTINATION "${python_package_dir}/ifcopenshell") endif() if(NOT WIN32 AND NOT WASM_BUILD) # On Linux $ORIGIN expands to the directory of the loaded # object (= site-packages/ifcopenshell/). macOS dyld doesn't # expand $ORIGIN; the equivalent placeholder is # @loader_path. Setting $ORIGIN on macOS bakes a literal # "$ORIGIN" into LC_RPATH which resolves to nothing at # runtime — the wrapper's `@rpath/ifcopenshell.document.rdb # .dylib` LC_LOAD_DYLIB then fails to find the plug-in even # though INSTALL(TARGETS … LIBRARY DESTINATION …/ifcopenshell) # above has dropped it right next to the wrapper. if(APPLE) set_target_properties(ifcopenshell_wrapper PROPERTIES INSTALL_RPATH "@loader_path" ) else() set_target_properties(ifcopenshell_wrapper PROPERTIES INSTALL_RPATH "$ORIGIN" ) endif() endif() endif() if (MSVC) INSTALL(FILES $ DESTINATION bin OPTIONAL) endif() ENDIF() ELSE() MESSAGE(WARNING "No Python interpreter found, unable to install the Python wrapper") ENDIF()