Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 121 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ set(GTWRAP_MATLAB_OPTION_NAME "${LIB_NAMESPACE}_BUILD_MATLAB_WRAPPER")
set(GTWRAP_INTERFACE_OPTION_NAME "${LIB_NAMESPACE}_WRAPPER_INTERFACE_FILES")
set(GTWRAP_TOP_NAMESPACE_OPTION_NAME "${LIB_NAMESPACE}_GTWRAP_TOP_NAMESPACE")
set(GTWRAP_DEPENDENCIES_OPTION_NAME "${LIB_NAMESPACE}_GTWRAP_DEPENDENCY_TARGETS")
set(GTWRAP_RUNTIME_DEPENDENCIES_OPTION_NAME
"${LIB_NAMESPACE}_GTWRAP_RUNTIME_DEPENDENCY_TARGETS")
set(GTWRAP_ROOT_OPTION_NAME "${LIB_NAMESPACE}_GTWRAP_ROOT_DIR")

option(${GTWRAP_PYTHON_OPTION_NAME} "Build Python wrapper" ${GTWRAP_BUILD_PYTHON_DEFAULT})
Expand Down Expand Up @@ -214,6 +216,14 @@ if (NOT DEFINED ${GTWRAP_DEPENDENCIES_OPTION_NAME})
FORCE)
endif()

if (NOT DEFINED ${GTWRAP_RUNTIME_DEPENDENCIES_OPTION_NAME})
set(${GTWRAP_RUNTIME_DEPENDENCIES_OPTION_NAME}
""
CACHE STRING
"Direct project-owned shared runtime build targets packaged beside the Python wrapper."
FORCE)
endif()

if (NOT DEFINED ${GTWRAP_ROOT_OPTION_NAME})
set(${GTWRAP_ROOT_OPTION_NAME}
""
Expand Down Expand Up @@ -419,10 +429,9 @@ set(ANY_WRAPPER_ENABLED OFF)
if(BUILD_AS_MAIN_PROJECT)
set(LIBNAME_WRAP_TARGET ${project_name})
handle_gtwrappers() # You may also pass WRAP_INTERFACE_FILES variable to specify interface files, else autofetching is used
handle_pybind11_wrapper()

# Determine if any wrapper is enabled
if (${GTWRAP_PYTHON_OPTION_NAME} OR PYBIND11_WRAPPER_ENABLED OR ${GTWRAP_MATLAB_OPTION_NAME})
if(${GTWRAP_PYTHON_OPTION_NAME} OR ${GTWRAP_MATLAB_OPTION_NAME})
set(ANY_WRAPPER_ENABLED ON)
endif()

Expand Down Expand Up @@ -496,7 +505,6 @@ print_compiler_flags_summary()
# Print info about wrappers if any
if (ANY_WRAPPER_ENABLED)
message(STATUS "================ Wrappers Configuration ======================")
message(STATUS "Python pybind11 direct wrapper : ${PYBIND11_WRAPPER_ENABLED}")
message(STATUS "Python gtwrapper : ${${GTWRAP_PYTHON_OPTION_NAME}}")
message(STATUS "MATLAB gtwrapper : ${${GTWRAP_MATLAB_OPTION_NAME}}")
message(STATUS "MATLAB default release : ${MATLAB_DEFAULT_RELEASE}")
Expand All @@ -505,6 +513,7 @@ if (ANY_WRAPPER_ENABLED)
message(STATUS "gtwrap interface option name : ${GTWRAP_INTERFACE_OPTION_NAME}")
message(STATUS "gtwrap interface files : ${${GTWRAP_INTERFACE_OPTION_NAME}}")
message(STATUS "gtwrap top namespace : ${${GTWRAP_TOP_NAMESPACE_OPTION_NAME}}")
message(STATUS "gtwrap runtime dependency targets : ${${GTWRAP_RUNTIME_DEPENDENCIES_OPTION_NAME}}")
message(STATUS "gtwrap root override : ${${GTWRAP_ROOT_OPTION_NAME}}")
endif()
message(STATUS "===============================================================")
Expand Down Expand Up @@ -536,16 +545,124 @@ if(DEFINED FULL_VERSION AND NOT "${FULL_VERSION}" STREQUAL "")
endif()
set(CPACK_GENERATOR "TGZ")
set(CPACK_SOURCE_GENERATOR "TGZ")
# Preserve source-ignore regexes verbatim and anchor known generated outputs
# beneath this checkout rather than matching adjacent directories.
set(CPACK_VERBATIM_VARIABLES YES)
set(_cpack_source_root_regex "${CMAKE_CURRENT_SOURCE_DIR}")
string(
REGEX REPLACE "([][+.*^$()|?\\\\])" "\\\\\\1"
_cpack_source_root_regex "${_cpack_source_root_regex}")
set(CPACK_SOURCE_IGNORE_FILES
"^${_cpack_source_root_regex}/\\.git/"
"^${_cpack_source_root_regex}/(.*/)?\\.git(/|$)"
"^${_cpack_source_root_regex}/build[^/]*/"
"^${_cpack_source_root_regex}/install/"
"^${_cpack_source_root_regex}/ros2/(build|install|log)/"
"^${_cpack_source_root_regex}/(.*/)?\\.pytest_cache/"
"^${_cpack_source_root_regex}/(.*/)?__pycache__/"
"^${_cpack_source_root_regex}/.*\\.py[cod]$")

# Exclude the active nested binary tree and other nested CMake builds whose
# caches prove that this exact checkout owns them. Directory names alone are
# insufficient because paths such as tools/build_helpers may be real sources.
get_filename_component(
_cpack_source_root_real
"${CMAKE_CURRENT_SOURCE_DIR}"
REALPATH)
get_filename_component(
_cpack_binary_root_real
"${CMAKE_BINARY_DIR}"
REALPATH)
file(
RELATIVE_PATH
_cpack_binary_relative_to_source
"${_cpack_source_root_real}"
"${_cpack_binary_root_real}")
set(_cpack_owned_build_directories)
if(NOT IS_ABSOLUTE "${_cpack_binary_relative_to_source}"
AND NOT "${_cpack_binary_relative_to_source}" MATCHES "^\\.\\.(/|$)"
AND NOT "${_cpack_binary_relative_to_source}" STREQUAL "")
list(APPEND
_cpack_owned_build_directories
"${CMAKE_BINARY_DIR}")
endif()

file(
GLOB_RECURSE
_cpack_cache_candidates
LIST_DIRECTORIES FALSE
"${CMAKE_CURRENT_SOURCE_DIR}/*/CMakeCache.txt")
foreach(_cpack_cache_candidate IN LISTS _cpack_cache_candidates)
file(
RELATIVE_PATH
_cpack_cache_relative_to_source
"${CMAKE_CURRENT_SOURCE_DIR}"
"${_cpack_cache_candidate}")
if("${_cpack_cache_relative_to_source}" MATCHES "^build[^/]*/"
OR "${_cpack_cache_relative_to_source}" MATCHES "^install/"
OR "${_cpack_cache_relative_to_source}"
MATCHES "^ros2/(build|install|log)/")
continue()
endif()

set(_cpack_cache_is_within_owned_build FALSE)
foreach(_cpack_known_build IN LISTS _cpack_owned_build_directories)
file(
RELATIVE_PATH
_cpack_cache_relative_to_build
"${_cpack_known_build}"
"${_cpack_cache_candidate}")
if(NOT IS_ABSOLUTE "${_cpack_cache_relative_to_build}"
AND NOT "${_cpack_cache_relative_to_build}" MATCHES "^\\.\\.(/|$)")
set(_cpack_cache_is_within_owned_build TRUE)
break()
endif()
endforeach()
if(_cpack_cache_is_within_owned_build
OR NOT EXISTS "${_cpack_cache_candidate}")
continue()
endif()

file(
STRINGS
"${_cpack_cache_candidate}"
_cpack_cache_home_entries
REGEX "^CMAKE_HOME_DIRECTORY:INTERNAL="
LIMIT_COUNT 1)
if(NOT _cpack_cache_home_entries)
continue()
endif()

list(GET _cpack_cache_home_entries 0 _cpack_cache_home_entry)
string(REGEX MATCH
"^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$"
_cpack_cache_home_match
"${_cpack_cache_home_entry}")
set(_cpack_cache_home "${CMAKE_MATCH_1}")
get_filename_component(
_cpack_cache_home_real
"${_cpack_cache_home}"
REALPATH)
if(NOT "${_cpack_cache_home_real}" STREQUAL "${_cpack_source_root_real}")
continue()
endif()

get_filename_component(
_cpack_owned_build_directory
"${_cpack_cache_candidate}"
DIRECTORY)
list(APPEND
_cpack_owned_build_directories
"${_cpack_owned_build_directory}")
endforeach()

list(REMOVE_DUPLICATES _cpack_owned_build_directories)
foreach(_cpack_owned_build_directory IN LISTS _cpack_owned_build_directories)
string(
REGEX REPLACE "([][+.*^$()|?\\\\])" "\\\\\\1"
_cpack_owned_build_regex "${_cpack_owned_build_directory}")
list(
APPEND
CPACK_SOURCE_IGNORE_FILES
"^${_cpack_owned_build_regex}(/|$)")
endforeach()
include(CPack)
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ All options are passed via `build_lib.sh` flags or directly as `-D<VAR>=<VAL>` t
-N, --ninja-build Use Ninja generator
-f, --flagsCXX "<flags>" Extra compiler flags (e.g. "-march=native")
-D, --define <VAR=VAL> Extra CMake cache definitions (repeatable)
--clean Delete build dir before configure
--clean Safely delete an owned in-repository build before configure
--profile Enable profiling build (see Profiling section)
--skip-tests Do not run tests after build
-i, --install Run install target after tests
Expand All @@ -186,6 +186,10 @@ All options are passed via `build_lib.sh` flags or directly as `-D<VAR>=<VAL>` t

See [`doc/build_script_doc.md`](doc/build_script_doc.md) for a detailed option reference.

`--clean` accepts only conventional in-repository `build`, `build*`, or
`out/*` paths. An existing directory must contain a CMake cache owned by this
checkout. The option is ignored with `--rebuild-only`.

### CMake feature flags

| Option | Default | Description |
Expand Down Expand Up @@ -371,6 +375,12 @@ into `python/pyproject.toml` when Python wrapping is requested.
The optional `setup.py.in` augments installation behavior without duplicating
package name/version metadata.

The wrapper wheel automatically co-locates the main project shared library.
Additional direct project-owned shared runtime build targets can be declared
through
`<namespace>_GTWRAP_RUNTIME_DEPENDENCY_TARGETS`; the separate
`<namespace>_GTWRAP_DEPENDENCY_TARGETS` option remains build-order-only.

The checked-in `python/<project>/__init__.py` is the public package entrypoint:

- `import <project>` is the supported import path.
Expand All @@ -383,7 +393,8 @@ entrypoint. CMake updates it with:

- generated `python/pyproject.toml`
- generated `python/setup.py`
- generated `python/<project>/_wrapper_build.py` linking the latest wrapper build
- build-time `python/<project>/_wrapper_build.py` linking the latest
successfully staged wrapper configuration

Install from the source Python package directory:

Expand Down
57 changes: 53 additions & 4 deletions build_lib.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Build helper for CMake-based C++ projects (Linux)
# - Created Jan 2024; updated Aug 2025
# - Created Jan 2024; updated Jul 2026
# - Uses GNU getopt for long options
# - Generator-agnostic build via `cmake --build`

Expand Down Expand Up @@ -221,6 +221,8 @@ Notes:
For CMake defines, use "-DVAR=ON" or "-D VAR=ON".
* Wrapper rebuilds with "-r -p" or "-r -m" only work if the existing build
directory was already configured with those wrappers enabled.
* "--clean" is ignored with "--rebuild-only". Otherwise it accepts only
conventional in-repository paths owned by this checkout's CMake cache.
* The default wrapper interface file is "src/wrap_interface.i". If it is
missing, wrapper generation is auto-disabled unless you pass a valid
*_WRAPPER_INTERFACE_FILES or *_WRAPPER_AUTODISCOVER_INTERFACE_FILES option.
Expand All @@ -236,6 +238,44 @@ info() { echo -e "\e[34m[INFO]\e[0m $*"; } # Print info
warn() { echo -e "\e[33m[WARN]\e[0m $*"; } # Print warning
trap 'echo -e "\e[31mBuild failed (line $LINENO).\e[0m"' ERR # Exit condition

# Normalize the requested clean path and prove that an existing directory is a
# conventional CMake build owned by the checkout in the current directory.
validate_clean_build_path() {
local project_root_
local relative_buildpath_
local build_cache_
local cached_source_dir_

# Constrain recursive removal to one CMake build owned by this checkout.
project_root_="$(pwd -P)"
buildpath="$(realpath -m "$buildpath")"
relative_buildpath_="${buildpath#"${project_root_}/"}"
if [[ "$relative_buildpath_" == "$buildpath" ]]; then
die "--clean requires a build directory inside '${project_root_}'"
fi
case "$relative_buildpath_" in
build|build/*|build[^/]*|out/*) ;;
*)
die "--clean requires a conventional build path (build, build*, or out/*)"
;;
esac

if [[ -e "$buildpath" ]]; then
build_cache_="${buildpath}/CMakeCache.txt"
[[ -f "$build_cache_" ]] ||
die "Refusing to clean a directory without a CMake cache: $buildpath"
cached_source_dir_="$(
sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' "$build_cache_" |
tail -n 1
)"
[[ -n "$cached_source_dir_" ]] ||
die "CMake source marker is missing from '$build_cache_'"
cached_source_dir_="$(realpath -m "$cached_source_dir_")"
[[ "$cached_source_dir_" == "$project_root_" ]] ||
die "Refusing to clean a build owned by '$cached_source_dir_'"
fi
}

# --- argument parsing (GNU getopt) ---
if ! command -v getopt > /dev/null 2>&1; then
die "GNU getopt is required. On macOS: brew install gnu-getopt and adjust PATH."
Expand Down Expand Up @@ -314,6 +354,10 @@ if [[ -n "$python_test_executable" && ! -x "$python_test_executable" ]]; then
die "Python test executable is not executable: $python_test_executable"
fi

if [[ "$clean_first" == true && "$rebuild_only" == false ]]; then
validate_clean_build_path
fi

project_name="$(detect_project_name || true)"
prepare_wrap_checkout=false

Expand Down Expand Up @@ -378,9 +422,14 @@ sleep 0.2

# --- Configure ---
if [[ "$rebuild_only" == false ]]; then
if [[ "$clean_first" == true && -d "$buildpath" ]]; then
info "Removing existing build dir '$buildpath'"
rm -rf -- "$buildpath"
if [[ "$clean_first" == true ]]; then
# Revalidate at the destructive boundary in case the path or cache changed
# while wrapper prerequisites were being prepared.
validate_clean_build_path
if [[ -d "$buildpath" ]]; then
info "Removing existing build dir '$buildpath'"
rm -rf -- "$buildpath"
fi
fi

cmake_args=(
Expand Down
53 changes: 53 additions & 0 deletions cmake/HandleMatlabWrapper.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Configure gtwrap MATLAB bindings and their generated toolbox output.
#
# This module owns MATLAB discovery and wrapper generation; common gtwrap
# checkout and interface resolution remains in HandleWrapper.cmake.
include_guard(GLOBAL)

# Configure the project's gtwrap-generated MATLAB wrapper.
function(configure_matlab_gtwrapper)
message(STATUS "Configuring MATLAB wrap...")

if(NOT GTWRAP_INTERFACE_FILES)
message(FATAL_ERROR
"GTWRAP_INTERFACE_FILES is empty. Cannot build MATLAB wrapper.")
endif()

if(NOT COMMAND wrap_and_install_library)
include(MatlabWrap)
endif()

message(STATUS "Including MATLAB directories...")
find_package(Matlab REQUIRED)
set(MATLAB_MEX_INCLUDE "${Matlab_ROOT_DIR}/extern/include")

message(STATUS "MATLAB_MEX_INCLUDE directory: ${MATLAB_MEX_INCLUDE}")
message(STATUS "Matlab_MEX_LIBRARY directory: ${Matlab_MEX_LIBRARY}")
message(STATUS "Matlab_MX_LIBRARY directory: ${Matlab_MX_LIBRARY}")

include_directories(${Matlab_INCLUDE_DIRS})
include_directories(${MATLAB_MEX_INCLUDE})
if(DEFINED GTWRAP_INCLUDE_DIR)
include_directories(${GTWRAP_INCLUDE_DIR})
endif()

if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/matlab")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/matlab")
endif()

set(WRAP_MEX_BUILD_STATIC_MODULE OFF)
set(WRAP_TOOLBOX_INSTALL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/matlab")
set(WRAP_BUILD_TYPE_POSTFIXES OFF)

if(NOT DEFINED LIBNAME_WRAP_TARGET)
message(FATAL_ERROR
"LIBNAME_WRAP_TARGET is not defined. Cannot configure the MATLAB "
"wrapper.")
endif()

message(STATUS "Using interface files: ${GTWRAP_INTERFACE_FILES}")
wrap_and_install_library(
"${GTWRAP_INTERFACE_FILES}"
"${LIBNAME_WRAP_TARGET}"
"" "" "" "" OFF)
endfunction()
Loading
Loading