diff --git a/.github/workflows/build_libraries.yml b/.github/workflows/build_libraries.yml index 8cc5645e5..561e71675 100644 --- a/.github/workflows/build_libraries.yml +++ b/.github/workflows/build_libraries.yml @@ -29,17 +29,13 @@ jobs: - name: Build libraries working-directory: lib/ - run: | - mkdir build - cd build - cmake .. - cmake --build . --config Release --target install + run: ./build.ps1 - name: Upload output folder uses: actions/upload-artifact@v7 with: name: libespp_windows - path: lib/pc + path: install build_linux: @@ -67,7 +63,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: libespp_linux - path: lib/pc + path: install build_macos: @@ -100,4 +96,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: libespp_macos - path: lib/pc + path: install diff --git a/.gitignore b/.gitignore index d50af212d..f3de3877c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,9 @@ dependencies.lock # weird mac folders... .DS_Store -lib/pc +# local espp staging install prefix produced by lib/build.sh (find_package tree +# + python package); consumed by pc/build.sh via CMAKE_PREFIX_PATH. +/install/ _build/ __pycache__/ managed_components/ diff --git a/components/rtps/interop/README.md b/components/rtps/interop/README.md index e6d08b36e..0ae835e91 100644 --- a/components/rtps/interop/README.md +++ b/components/rtps/interop/README.md @@ -13,7 +13,9 @@ cd components/rtps/interop Requires docker. One container (`ros:jazzy-ros-base` = FastDDS + rmw_fastrtps + `ros2` CLI) runs everything in a single network namespace, so RTPS multicast works unconditionally. The repo is bind-mounted and copied to a container-local tree -before building, so your host `lib/pc` artifacts are never touched. +before building (espp is installed into a container-local staging prefix and the +pc tests `find_package` it from there), so your host build artifacts are never +touched. ## Matrix diff --git a/components/rtps/interop/run_interop.sh b/components/rtps/interop/run_interop.sh index 77a4d5ca1..d10baaa22 100755 --- a/components/rtps/interop/run_interop.sh +++ b/components/rtps/interop/run_interop.sh @@ -6,10 +6,10 @@ # # NOTE: no `set -u` - ROS 2's setup.bash references unset variables. -# Work on a container-local copy: the pc tests link the lib installed into -# lib/pc inside the source tree, which on the bind mount holds the developer's -# host-platform (e.g. macOS) artifacts. Building in-place would either link -# incompatible objects or clobber them with linux ones. +# Work on a container-local copy: the build installs espp into a staging prefix +# (/tmp/espp/install) and the pc tests find_package it from there. Doing this on +# the bind mount would either mix in the developer's host-platform (e.g. macOS) +# artifacts or clobber them with linux ones, so copy to a container-local tree. echo "===== Copy sources to container-local tree =====" rsync -a --delete --exclude '.git/' --exclude 'build/' --exclude 'build-*/' --exclude 'managed_components/' --exclude 'docs/' --exclude 'dependencies.lock' /work/ /tmp/espp/ cd /tmp/espp @@ -23,9 +23,9 @@ result() { # name exit_code } note "Build espp lib + host binaries (linux)" -cmake -S lib -B lib/build -DCMAKE_BUILD_TYPE=Release > /tmp/cmake_lib.log 2>&1 \ +cmake -S lib -B lib/build -DCMAKE_BUILD_TYPE=Release -DESPP_INSTALL=ON -DCMAKE_INSTALL_PREFIX=/tmp/espp/install > /tmp/cmake_lib.log 2>&1 \ && cmake --build lib/build -j"$(nproc)" --target install > /tmp/build_lib.log 2>&1 \ - && cmake -S pc -B pc/build -DCMAKE_BUILD_TYPE=Release > /tmp/cmake.log 2>&1 \ + && cmake -S pc -B pc/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/tmp/espp/install > /tmp/cmake.log 2>&1 \ && cmake --build pc/build -j"$(nproc)" --target \ rtps_pubsub rtps_golden rtps_facade_pubsub rtps_typed_pubsub \ rtps_facade_frag rtps_facade_backlog rtps_facade_frag_sizes rtps_service_loopback \ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 7bfd5ba42..34f2ad8fd 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,23 +1,95 @@ -cmake_minimum_required(VERSION 3.20) +# 3.21+ for a reliable PROJECT_IS_TOP_LEVEL (used to default ESPP_INSTALL to ON +# only for a top-level build, so a FetchContent/CPM consumer's `cmake --install` +# never installs espp into the parent's prefix unless it opts in). +cmake_minimum_required(VERSION 3.21) -# building PC c++ library and python binding -message(STATUS "Building for PC: C++ & Python") +# --------------------------------------------------------------------------- +# Package version, derived from the latest git tag (strip a leading 'v' and keep +# the MAJOR.MINOR.PATCH numeric core). This flows into PROJECT_VERSION and thus +# esppConfigVersion.cmake, so `find_package(espp X.Y.Z)` version checks work. +# Falls back to 0.0.0 when git or a tag is unavailable (e.g. tarball builds) so +# configure still succeeds. +# +# NOTE: the python wheel gets its version independently from setuptools_scm (see +# pyproject.toml [tool.setuptools_scm] / SKBUILD_PROJECT_VERSION); this git-tag +# derivation is only for the CMake / find_package package version. +# --------------------------------------------------------------------------- +set(ESPP_VERSION "0.0.0") +find_package(Git QUIET) +if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=0 + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE _espp_git_tag + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _espp_git_result) + if(_espp_git_result EQUAL 0 AND _espp_git_tag) + string(REGEX REPLACE "^v" "" _espp_git_tag "${_espp_git_tag}") + if(_espp_git_tag MATCHES "^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?") + set(ESPP_VERSION "${CMAKE_MATCH_0}") + endif() + endif() +endif() + +# Build the PC (host) C++ static library. Python bindings are opt-in (see the +# ESPP_BUILD_PYTHON option below); a plain `cmake -S lib` is C++-only. +project(espp VERSION ${ESPP_VERSION}) +message(STATUS "espp package version: ${PROJECT_VERSION} (from git tag; 0.0.0 = no tag)") + +# Build the espp Python bindings (_espp)? Default OFF so a plain +# `cmake -S lib` produces just the C++ static library + its install/export +# (find_package(espp) / espp::espp) with no pybind11 dependency. The standalone +# scripts (build.sh / build.ps1) pass -DESPP_BUILD_PYTHON=ON to also build and +# install the python package (the `espp/` package) alongside the C++ package +# under CMAKE_INSTALL_PREFIX (this is what CI publishes). Wheel builds via +# scikit-build-core take the SKBUILD path below regardless of this option. +option(ESPP_BUILD_PYTHON "Build the espp Python bindings module (_espp)" OFF) + +# Install the standard, relocatable CMake package (install(TARGETS ... EXPORT), +# esppConfig.cmake, flattened headers) so a separate project can +# `find_package(espp)` and link `espp::espp`. This is THE install path (there is +# no longer a legacy source-tree lib/pc install). Defaults to ON for a top-level +# build (build.sh passes an explicit CMAKE_INSTALL_PREFIX, so no /usr/local +# surprise) and OFF when espp is a subproject, so a FetchContent/CPM consumer's +# `cmake --install` does not drag espp into the parent's prefix unless asked. +option(ESPP_INSTALL "Install espp as a find_package-able package (standard install/export under CMAKE_INSTALL_PREFIX)" ${PROJECT_IS_TOP_LEVEL}) + +# `SKBUILD` is set for ANY scikit-build-core configure in the tree - including +# when a DOWNSTREAM scikit-build-core project pulls espp in via FetchContent. In +# that case espp is not the top-level project and must still build the normal C++ +# static library + the advertised `espp::espp` target, NOT just the `_espp` wheel +# module. So the wheel-only path is taken only when espp itself is the top-level +# scikit-build project (i.e. `pip wheel .` of espp). +if(SKBUILD AND PROJECT_IS_TOP_LEVEL) + set(ESPP_WHEEL_BUILD ON) +else() + set(ESPP_WHEEL_BUILD OFF) +endif() -project(espp) +# Report what this configure will actually build (Python is opt-in). +if(ESPP_WHEEL_BUILD OR ESPP_BUILD_PYTHON) + message(STATUS "Building espp for PC: C++ static library + Python bindings (_espp)") +else() + message(STATUS "Building espp for PC: C++ static library only (set -DESPP_BUILD_PYTHON=ON for Python bindings)") +endif() # Prefer an installed pybind11 (provided as a build requirement when building # python wheels via pip / scikit-build-core); fall back to fetching it with CPM -# for standalone CMake builds (e.g. ./build.sh). -find_package(pybind11 CONFIG QUIET) -if(NOT pybind11_FOUND) - include(cmake/CPM.cmake) - CPMAddPackage( - NAME pybind11 - GIT_REPOSITORY https://github.com/pybind/pybind11.git - VERSION 3.0.4 - ) - if(NOT pybind11_ADDED) - message(FATAL_ERROR "pybind11 not found. Please ensure it is available in the specified version.") +# for standalone CMake builds (e.g. ./build.sh). Only needed when the python +# bindings are actually being built. +if(ESPP_WHEEL_BUILD OR ESPP_BUILD_PYTHON) + find_package(pybind11 CONFIG QUIET) + if(NOT pybind11_FOUND) + include(cmake/CPM.cmake) + CPMAddPackage( + NAME pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11.git + VERSION 3.0.4 + ) + if(NOT pybind11_ADDED) + message(FATAL_ERROR "pybind11 not found. Please ensure it is available in the specified version.") + endif() endif() endif() @@ -27,20 +99,20 @@ include(espp.cmake) include_directories(${ESPP_INCLUDE_DIRS}) -set(LINK_ARG "--whole-archive") - # settings for Windows / MSVC if(MSVC) add_compile_options(/utf-8 /D_USE_MATH_DEFINES /bigobj) add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif() -# settings for MacOS -if(APPLE) - set(LINK_ARG "-all_load") -endif() +# NOTE: whole-archive linking (so global-ctor / registration code such as the +# Windows timer-period adjustment in espp.hpp is not stripped) is now the +# CONSUMER's responsibility, applied where espp::espp is linked (see +# pc/CMakeLists.txt, which wraps it with $). It +# is a no-op on the static archive itself (ar ignores link flags), so it does +# not belong here. -if(SKBUILD) +if(ESPP_WHEEL_BUILD) # Building a python wheel via scikit-build-core (pip install). Only build the # espp._espp extension module and install it into the `espp` package; the # pure-python package files come from lib/python_bindings/espp via the @@ -51,7 +123,8 @@ if(SKBUILD) RUNTIME DESTINATION espp) else() # Standalone build (./build.sh / ./build.ps1): build the C++ static library - # and the python package, and install both into ./pc for local use. + # (and, with -DESPP_BUILD_PYTHON=ON, the python package) and install the + # find_package-able package into CMAKE_INSTALL_PREFIX. set(TARGET_NAME "espp_pc") # main library (which can be built for pc, android, and iOS) @@ -61,20 +134,53 @@ else() STATIC # Provides a relative path to your source file(s). ${ESPP_SOURCES} ) + # Namespaced ALIAS so build-tree (FetchContent / CPM add_subdirectory) and + # install-tree (find_package) consumers link the SAME name: espp::espp. + add_library(espp::espp ALIAS ${TARGET_NAME}) + # Export the target as `espp` (not `espp_pc`) so find_package consumers link + # `espp::espp` too -- the SAME name as the build-tree alias above. The on-disk + # archive name stays libespp_pc.a (OUTPUT_NAME unchanged) for pc/ and CI. + set_target_properties(${TARGET_NAME} PROPERTIES EXPORT_NAME espp) set_property(TARGET ${TARGET_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) - target_link_options(${TARGET_NAME} PRIVATE "${LINK_ARG}") - target_link_libraries(${TARGET_NAME} ${ESPP_EXTERNAL_LIBS}) + # PUBLIC so the system link deps (pthread, or ws2_32/winmm/iphlpapi on Windows) + # propagate to consumers of the exported target. + target_link_libraries(${TARGET_NAME} PUBLIC ${ESPP_EXTERNAL_LIBS}) if(WIN32) - target_link_libraries(${TARGET_NAME} winmm) + target_link_libraries(${TARGET_NAME} PUBLIC winmm) endif() - target_compile_features(${TARGET_NAME} PRIVATE cxx_std_23) + # PUBLIC/INTERFACE so consumers of espp::espp automatically compile as C++23 + # (the espp headers require it). + target_compile_features(${TARGET_NAME} PUBLIC cxx_std_23) + # PUBLIC so find_package(espp) consumers compile the rtps headers with the SAME + # limits/fragmentation profile the archive was built with (ABI-critical; see + # espp.cmake). These export into esppTargets.cmake as INTERFACE definitions. + target_compile_definitions(${TARGET_NAME} PUBLIC ${ESPP_RTPS_COMPILE_DEFINITIONS}) - # install build output and headers - install(TARGETS ${TARGET_NAME} - ARCHIVE DESTINATION ${PROJECT_SOURCE_DIR}/pc) + # --------------------------------------------------------------------------- + # Usage requirements (modern, target-based). Expose every include dir espp.cmake + # collects as a BUILD_INTERFACE dir (so FetchContent/CPM consumers using the + # build tree get them), and /include as the INSTALL_INTERFACE dir. + # --------------------------------------------------------------------------- + include(GNUInstallDirs) + foreach(_dir IN LISTS ESPP_INCLUDE_DIRS) + target_include_directories(${TARGET_NAME} PUBLIC $) + endforeach() + target_include_directories(${TARGET_NAME} + PUBLIC $) - espp_install_includes(${PROJECT_SOURCE_DIR}/pc) + # --------------------------------------------------------------------------- + # Proper install + export so `find_package(espp)` works from a separate + # project. Installs into GNUInstallDirs under CMAKE_INSTALL_PREFIX. This is THE + # install path; ESPP_INSTALL defaults ON for a top-level build (build.sh passes + # an explicit prefix) and OFF for a subproject. + # --------------------------------------------------------------------------- + if(ESPP_INSTALL) + espp_install_cmake_package(${TARGET_NAME}) + endif() # Build and install the python package (espp/ with the _espp extension inside) - espp_install_python_module(${PROJECT_SOURCE_DIR}/pc) + # into CMAKE_INSTALL_PREFIX (put on PYTHONPATH to `import espp`). + if(ESPP_BUILD_PYTHON) + espp_install_python_module() + endif() endif() diff --git a/lib/README.md b/lib/README.md index ea57f83e6..c9f649308 100644 --- a/lib/README.md +++ b/lib/README.md @@ -75,26 +75,57 @@ PyPI on each release. ## Building for PC (C++ & Python) -To build the library for use on PC (with C++ and Python), simply build with -cmake: +To build the library for use on PC (with C++ and Python), install it into a +staging prefix with cmake: ``` sh -mkdir build -cd build -cmake .. -cmake --build . --config Release --target install +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DESPP_INSTALL=ON -DESPP_BUILD_PYTHON=ON \ + -DCMAKE_INSTALL_PREFIX=../install +cmake --build build --config Release --target install --parallel 4 ``` This is conveniently scripted up for you into [./build.sh](./build.sh) and -[./build.ps1](./build.ps1) scripts you can simply run from your terminal. +[./build.ps1](./build.ps1) scripts you can simply run from your terminal; they +install into `/install`. + +This installs a standard, relocatable CMake package plus the python package +into the prefix: + +* `/lib/libespp_pc.a` - C++ static library. +* `/include/` - all the header files needed to use the library from C++. +* `/lib/cmake/espp/` - `esppConfig.cmake` + friends, so another project + can `find_package(espp)` and link the `espp::espp` target (see below). +* `/espp/` - the `espp` python package (pure-python files, type stubs, + and the compiled `espp._espp` pybind11 extension) - add `` to your + `PYTHONPATH` / `sys.path` to `import espp` from python code. -This will build and install the following files: +The package version is derived from the latest git tag at configure time (a +leading `v` is stripped), and falls back to `0.0.0` for tarball / no-git builds. +The python wheel's version comes separately from `setuptools_scm`. -* `./pc/libespp_pc` - C++ static library for use with other C++ code. -* `./pc/include` - All the header files need for using the library from C++ code. -* `./pc/espp/` - The `espp` python package (pure-python files, type stubs, and - the compiled `espp._espp` pybind11 extension) - add `./pc` to your - `PYTHONPATH` / `sys.path` to `import espp` from python code. +### Using espp from another C++ project (find_package) + +Point `CMAKE_PREFIX_PATH` at the install prefix and link the `espp::espp` +target - it carries the include dirs, the C++23 requirement, and the PUBLIC +system libraries, so nothing else is needed: + +``` cmake +find_package(espp REQUIRED) +target_link_libraries(my_app PRIVATE espp::espp) +# If your app relies on espp's global-ctor / registration code (e.g. the Windows +# timer-period adjustment), whole-archive it (CMake 3.24+): +# target_link_libraries(my_app PRIVATE "$") +``` + +``` sh +cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/install +``` + +The same `espp::espp` target is also available without installing, via +`FetchContent` / CPM `add_subdirectory` of `lib/` (build-tree consumers get the +component headers directly). The [../pc](../pc) example tests consume the +installed package this way. ## Updating the python bindings diff --git a/lib/build.ps1 b/lib/build.ps1 index 315749b2e..b812b37aa 100644 --- a/lib/build.ps1 +++ b/lib/build.ps1 @@ -1,19 +1,19 @@ -# powershell script to build the project using cmake +# powershell script to build espp and install the find_package-able package +# (C++ static library + headers + esppConfig.cmake) together with the python +# `espp` package into a local staging prefix (/install). Point ../pc (and +# any external consumer) at it with -DCMAKE_PREFIX_PATH=/install. -# Create build directory if it doesn't exist -$buildDir = "build" -if (-not (Test-Path -Path $buildDir)) { - New-Item -ItemType Directory -Path $buildDir -} +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$prefix = Join-Path $repoRoot "install" +$buildDir = Join-Path $scriptDir "build" -# Change to the build directory -Set-Location -Path $buildDir +# ESPP_BUILD_PYTHON=ON also builds/installs the python package (what CI +# publishes). ESPP_INSTALL=ON installs the find_package package into $prefix. +cmake -S $scriptDir -B $buildDir ` + -DCMAKE_BUILD_TYPE=Release ` + -DESPP_INSTALL=ON ` + -DESPP_BUILD_PYTHON=ON ` + -DCMAKE_INSTALL_PREFIX=$prefix -# Run cmake -cmake .. - -# Run cmake --build . --config Release --target install -cmake --build . --config Release --target install --parallel 4 - -# Change back to the original directory -Set-Location -Path .. +cmake --build $buildDir --config Release --target install --parallel 4 diff --git a/lib/build.sh b/lib/build.sh index eff1744e3..433c64814 100755 --- a/lib/build.sh +++ b/lib/build.sh @@ -1,6 +1,20 @@ #!/bin/bash +set -e -mkdir build -cd build -cmake .. -cmake --build . --config Release --target install --parallel 4 +# Build espp and install the find_package-able package (C++ static library + +# headers + esppConfig.cmake) together with the python `espp` package into a +# local staging prefix (/install). Point ../pc (and any external consumer) +# at it with -DCMAKE_PREFIX_PATH=/install; put /install on PYTHONPATH +# to `import espp`. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PREFIX="$REPO_ROOT/install" + +# ESPP_BUILD_PYTHON=ON also builds/installs the python package (what CI +# publishes). ESPP_INSTALL=ON installs the find_package package into PREFIX. +cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DESPP_INSTALL=ON \ + -DESPP_BUILD_PYTHON=ON \ + -DCMAKE_INSTALL_PREFIX="$PREFIX" +cmake --build "$SCRIPT_DIR/build" --config Release --target install --parallel 4 diff --git a/lib/cmake/esppConfig.cmake.in b/lib/cmake/esppConfig.cmake.in new file mode 100644 index 000000000..269fe0d78 --- /dev/null +++ b/lib/cmake/esppConfig.cmake.in @@ -0,0 +1,15 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# espp's only non-bundled, PUBLIC runtime dependency is the system threads +# library (pthread on POSIX). Every other third-party library used by espp +# (reflect-cpp, magic_enum, tabulate, fmt, alpaca, cli, csv2, hid-rp, cdr) is +# vendored: its headers are installed under /include and its objects are +# compiled into libespp_pc.a, so no find_dependency is required for those. On +# Windows the system libs (ws2_32, winmm, iphlpapi) resolve automatically. +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/esppTargets.cmake") + +check_required_components(espp) diff --git a/lib/espp.cmake b/lib/espp.cmake index 7bc03fa38..ebeeb624c 100644 --- a/lib/espp.cmake +++ b/lib/espp.cmake @@ -24,19 +24,20 @@ set(RTPS_LIMITS_PROFILE "${RTPS_LIMITS_PROFILE}" CACHE STRING set_property(CACHE RTPS_LIMITS_PROFILE PROPERTY STRINGS embedded host host_large) if(RTPS_LIMITS_PROFILE STREQUAL "embedded") - add_compile_definitions(RTPS_CONFIG_HEADER="rtps/config_esp32.hpp") + set(RTPS_CONFIG_HEADER_FILE "rtps/config_esp32.hpp") set(RTPS_MAX_SAMPLE_SIZE 262144) # 256 KB (matches config_esp32.hpp) elseif(RTPS_LIMITS_PROFILE STREQUAL "host") - add_compile_definitions(RTPS_CONFIG_HEADER="rtps/config_desktop.hpp") + set(RTPS_CONFIG_HEADER_FILE "rtps/config_desktop.hpp") set(RTPS_MAX_SAMPLE_SIZE 8388608) # 8 MB (matches config_desktop.hpp) elseif(RTPS_LIMITS_PROFILE STREQUAL "host_large") - add_compile_definitions(RTPS_CONFIG_HEADER="rtps/config_host_large.hpp") + set(RTPS_CONFIG_HEADER_FILE "rtps/config_host_large.hpp") set(RTPS_MAX_SAMPLE_SIZE 8388608) # 8 MB (matches config_host_large.hpp) else() message(FATAL_ERROR "Invalid RTPS_LIMITS_PROFILE '${RTPS_LIMITS_PROFILE}' " "(expected: embedded | host | host_large)") endif() +add_compile_definitions(RTPS_CONFIG_HEADER="${RTPS_CONFIG_HEADER_FILE}") message(STATUS "RTPS limits profile: ${RTPS_LIMITS_PROFILE}") # --------------------------------------------------------------------------- @@ -53,6 +54,18 @@ message(STATUS "RTPS limits profile: ${RTPS_LIMITS_PROFILE}") add_compile_definitions(RTPS_ENABLE_FRAGMENTATION RTPS_MAX_SAMPLE_SIZE=${RTPS_MAX_SAMPLE_SIZE}) message(STATUS "RTPS fragmentation: ON (max sample size ${RTPS_MAX_SAMPLE_SIZE} bytes)") +# The RTPS static-limits + fragmentation config is compiled into libespp_pc.a AND +# is part of the public ABI: any consumer that compiles rtps headers (templates / +# inline code) must use the SAME profile, else it sees a different Config and a +# different set of guarded declarations (e.g. addSubMessageDataFrag). Expose them +# as PUBLIC compile definitions on the exported target (see lib/CMakeLists.txt) so +# find_package(espp) consumers inherit them automatically. The above +# add_compile_definitions still covers the in-tree python module / SKBUILD build. +set(ESPP_RTPS_COMPILE_DEFINITIONS + RTPS_CONFIG_HEADER="${RTPS_CONFIG_HEADER_FILE}" + RTPS_ENABLE_FRAGMENTATION + RTPS_MAX_SAMPLE_SIZE=${RTPS_MAX_SAMPLE_SIZE}) + set(ESPP_EXTERNAL_INCLUDES ${ESPP_COMPONENTS}/serialization/detail/alpaca/include ${ESPP_COMPONENTS}/cli/detail/cli/include @@ -184,12 +197,56 @@ set(ESPP_PYTHON_SOURCES ${ESPP_SOURCES} ) -# make an espp_install_includes command that can be used by other scripts, where -# they just need to specify the folder they want to install into -function(espp_install_includes FOLDER) - install(DIRECTORY ${ESPP_INCLUDES} DESTINATION ${FOLDER}/) - install(DIRECTORY ${ESPP_EXTERNAL_INCLUDES} DESTINATION ${FOLDER}/) - install(DIRECTORY ${ESPP_EXTERNAL_INCLUDES_SEPARATE} DESTINATION ${FOLDER}/include/) +# make an espp_install_cmake_package command that gives the C++ library target a +# proper install + export so a separate project can `find_package(espp)` and link +# `espp::espp`. Installs into GNUInstallDirs under CMAKE_INSTALL_PREFIX: +# /lib/libespp_pc.a +# /include/... (all component + vendored headers, flat) +# /lib/cmake/espp/esppTargets.cmake, esppConfig.cmake, ...Version.cmake +# +# All third-party deps (reflect-cpp, magic_enum, tabulate, fmt, alpaca, cli, +# csv2, hid-rp, cdr) are VENDORED: their headers are installed under +# /include and their objects are compiled into libespp_pc.a, so a +# consumer needs no extra find_package for them. The only non-bundled PUBLIC +# dependency is the system threads library (handled via find_dependency(Threads) +# in esppConfig.cmake.in; on Windows the ws2_32/winmm/iphlpapi system libs +# resolve automatically). +function(espp_install_cmake_package TARGET_NAME) + include(GNUInstallDirs) + include(CMakePackageConfigHelpers) + + install(TARGETS ${TARGET_NAME} + EXPORT esppTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + # Install the public headers the library exposes, merged flat into + # /include (mirrors the historical lib/pc/include layout, so the same + # quote-includes such as "logger.hpp" / "magic_enum.hpp" resolve). A trailing + # slash on the source installs the directory CONTENTS. + foreach(_inc IN LISTS ESPP_INCLUDES ESPP_EXTERNAL_INCLUDES ESPP_EXTERNAL_INCLUDES_SEPARATE) + install(DIRECTORY ${_inc}/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + endforeach() + + install(EXPORT esppTargets + NAMESPACE espp:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/espp + FILE esppTargets.cmake) + + configure_package_config_file( + ${CMAKE_CURRENT_LIST_DIR}/cmake/esppConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/esppConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/espp) + write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/esppConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/esppConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/esppConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/espp) endfunction() # make an espp_add_python_module command that defines the `_espp` pybind11 @@ -213,17 +270,18 @@ function(espp_add_python_module) endif() endfunction() -# make an espp_install_python_module command that can be used by other scripts, -# where they just need to specify the folder they want to install into. This -# installs the full `espp` python package (pure-python files + the compiled -# `_espp` extension) into FOLDER/espp so FOLDER can be put on sys.path. -function(espp_install_python_module FOLDER) +# make an espp_install_python_module command that installs the full `espp` python +# package (pure-python files + the compiled `_espp` extension) into the standard +# install prefix as /espp, so CMAKE_INSTALL_PREFIX can be put on sys.path +# / PYTHONPATH to `import espp`. Relative DESTINATIONs are interpreted against +# CMAKE_INSTALL_PREFIX. +function(espp_install_python_module) espp_add_python_module() install(DIRECTORY ${ESPP_PYTHON_BINDINGS_DIR}/espp - DESTINATION ${FOLDER}/ + DESTINATION . PATTERN "__pycache__" EXCLUDE PATTERN ".mypy_cache" EXCLUDE) install(TARGETS _espp - LIBRARY DESTINATION ${FOLDER}/espp/ - RUNTIME DESTINATION ${FOLDER}/espp/) + LIBRARY DESTINATION espp/ + RUNTIME DESTINATION espp/) endfunction() diff --git a/pc/CMakeLists.txt b/pc/CMakeLists.txt index ef907e3d8..1a7fec227 100644 --- a/pc/CMakeLists.txt +++ b/pc/CMakeLists.txt @@ -1,9 +1,22 @@ -cmake_minimum_required (VERSION 3.11) +# 3.24+ for the $ generator expression used +# below (portable whole-archive linking across GNU ld / Apple / MSVC). +cmake_minimum_required (VERSION 3.24) -set(CMAKE_CXX_STANDARD 23) +# Enable CXX at the top level so find_package(espp) -> find_dependency(Threads) +# (FindThreads needs an enabled C/CXX language) resolves. Individual tests still +# declare their own project()/name inside GEN_TESTS below. +project(espp_pc_tests LANGUAGES CXX) -include(${CMAKE_CURRENT_SOURCE_DIR}/../lib/espp.cmake) +set(CMAKE_CXX_STANDARD 23) +# Consume the INSTALLED espp package. Configure with +# -DCMAKE_PREFIX_PATH= +# where is where `cmake --install` put espp (see +# ../lib/build.sh, which installs into /install). The exported espp::espp +# target already carries its include dirs, C++23 requirement, and PUBLIC system +# libraries (pthread, or ws2_32/winmm/iphlpapi on Windows), so nothing else is +# needed here. +find_package(espp REQUIRED) MACRO(GEN_TESTS curdir) # get test files @@ -20,24 +33,17 @@ MACRO(GEN_TESTS curdir) add_compile_options(/utf-8 /D_USE_MATH_DEFINES /bigobj) add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif() - add_executable(${TEST_NAME} ${test_file}) - + add_executable(${TEST_NAME} ${test_file}) - target_include_directories(${TEST_NAME} PRIVATE - ${curdir}/../lib/pc/include) - target_link_directories(${TEST_NAME} PRIVATE - ${curdir}/../lib/pc) + # Link the WHOLE archive so global-ctor / registration code is not stripped + # from the static library -- e.g. the Windows timer-period adjustment in + # espp.hpp, which otherwise gets dropped and caps the timer at ~64 Hz. The + # $ genex expands to the correct per-platform + # flags around espp::espp only (GNU: -Wl,--whole-archive/--no-whole-archive, + # Apple: -force_load, MSVC: /WHOLEARCHIVE), and pulls in the target's PUBLIC + # usage requirements (includes, C++23, system libs) too. target_link_libraries(${TEST_NAME} - PRIVATE espp_pc - PRIVATE ${ESPP_EXTERNAL_LIBS} - ) - # /WHOLEARCHIVE is an MSVC/link.exe flag (gate on MSVC, not WIN32, so - # MinGW/clang Windows builds don't receive it). It ensures the whole archive - # is linked in, otherwise the Windows timer-period adjustment code (from - # espp.hpp) is stripped and the timer runs at a max of ~64 Hz. - if(MSVC) - target_link_options(${TEST_NAME} PRIVATE "/WHOLEARCHIVE:espp_pc.lib") - endif() + PRIVATE "$") ENDFOREACH() ENDMACRO() diff --git a/pc/README.md b/pc/README.md index 5475a8167..d2a46b4a3 100644 --- a/pc/README.md +++ b/pc/README.md @@ -15,9 +15,20 @@ Linux, MacOS, or Windows. ## Setup -First, ensure that you have built the shared objects in the `espp/lib` folder. -If you haven't done so yet, navigate to the `espp/lib` folder and run the -following: +First, install the espp library from the `espp/lib` folder. If you haven't done +so yet, navigate to the `espp/lib` folder and run the following, which installs +espp into `/install`: + +```console +# if macos/linux: +./build.sh +# if windows +./build.ps1 +``` + +Then build the tests here; they `find_package(espp)` from that install prefix +(the [./build.sh](./build.sh) / [./build.ps1](./build.ps1) scripts pass +`-DCMAKE_PREFIX_PATH=/install` for you): ```console # if macos/linux: diff --git a/pc/build.ps1 b/pc/build.ps1 index 08a8ae7cb..42825e09e 100644 --- a/pc/build.ps1 +++ b/pc/build.ps1 @@ -1,19 +1,14 @@ -# powershell script to build the project using cmake +# powershell script to build the pc/ example tests against the INSTALLED espp +# package. Run ../lib/build.ps1 first; it installs espp into /install, +# which we point find_package(espp) at via CMAKE_PREFIX_PATH. -# Create build directory if it doesn't exist -$buildDir = "build" -if (-not (Test-Path -Path $buildDir)) { - New-Item -ItemType Directory -Path $buildDir -} +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$prefix = Join-Path $repoRoot "install" +$buildDir = Join-Path $scriptDir "build" -# Change to the build directory -Set-Location -Path $buildDir +cmake -S $scriptDir -B $buildDir ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_PREFIX_PATH=$prefix -# Run cmake -cmake .. - -# Run cmake --build . --config Release -cmake --build . --config Release - -# Change back to the original directory -Set-Location -Path .. +cmake --build $buildDir --config Release --parallel 4 diff --git a/pc/build.sh b/pc/build.sh index 15ab2d0db..ddfd338be 100755 --- a/pc/build.sh +++ b/pc/build.sh @@ -1,6 +1,14 @@ #!/bin/bash +set -e -mkdir build -cd build -cmake .. -cmake --build . --config Release +# Build the pc/ example tests against the INSTALLED espp package. Run +# ../lib/build.sh first; it installs espp into /install, which we point +# find_package(espp) at via CMAKE_PREFIX_PATH. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PREFIX="$REPO_ROOT/install" + +cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$PREFIX" +cmake --build "$SCRIPT_DIR/build" --config Release --parallel 4 diff --git a/pyproject.toml b/pyproject.toml index 9c860363d..327535f91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ sdist.exclude = [ "docs/", "pc/", "python/", - # local build helpers / outputs (lib/pc, build/, _build/ already gitignored) + # local build helpers / outputs (install/, build/, _build/ already gitignored) "*.sh", "*.ps1", # embedded-only submodules and components not referenced by lib/espp.cmake