diff --git a/.github/workflows/ubuntu-build.yaml b/.github/workflows/ubuntu-build.yaml index 7ff7d42..4fd0533 100644 --- a/.github/workflows/ubuntu-build.yaml +++ b/.github/workflows/ubuntu-build.yaml @@ -48,6 +48,16 @@ jobs: working-directory: build run: ctest --output-on-failure + - name: Test embedding as a subproject + run: | + cmake -S cloudini_lib/test/subproject -B "$RUNNER_TEMP/subproject" + cmake --build "$RUNNER_TEMP/subproject" -j$(nproc) + "$RUNNER_TEMP/subproject/consumer" + cmake --install "$RUNNER_TEMP/subproject" --prefix "$RUNNER_TEMP/subproject-prefix" + if find "$RUNNER_TEMP/subproject-prefix" -iname '*cloudini*' | grep -q .; then + echo "embedded Cloudini added install rules to the parent project"; exit 1 + fi + - name: Test installed static and shared packages shell: bash run: | diff --git a/cloudini_lib/CHANGELOG.rst b/cloudini_lib/CHANGELOG.rst index ba37b08..c0935b6 100644 --- a/cloudini_lib/CHANGELOG.rst +++ b/cloudini_lib/CHANGELOG.rst @@ -2,6 +2,18 @@ Changelog for package cloudini_lib ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1.3.1 (2026-09-20) +------------------ +* build: make cloudini_lib embeddable with add_subdirectory / FetchContent (`#142 `_) + Reuses zstd/lz4 targets defined by a parent project, ignores an inherited ament_cmake, + and keeps the build type, tests, tools, benchmarks, PCL and install rules out of the + parent. New options CLOUDINI_WITH_PCL and CLOUDINI_INSTALL; cloudini::cloudini_lib is + always defined. +* fix: link Threads::Threads (static consumers failed with undefined pthread_create) +* fix: reject inconsistent point clouds; make ros_message_definitions.hpp includable twice (`#141 `_) +* The CMake project version now matches package.xml (it was left at 1.2.4 in 1.3.0) +* Contributors: Davide Faconti + 1.3.0 (2026-09-20) ------------------ * feat(packaging): conda/pixi package for prefix.dev + conda-forge (`#133 `_) diff --git a/cloudini_lib/CMakeLists.txt b/cloudini_lib/CMakeLists.txt index f68d865..9b561d0 100644 --- a/cloudini_lib/CMakeLists.txt +++ b/cloudini_lib/CMakeLists.txt @@ -1,7 +1,17 @@ cmake_minimum_required(VERSION 3.16) # NOTE: keep the default languages (C and CXX). The vendored zstd/lz4 fallback # compiles .c sources, so restricting to CXX only would break standalone builds. -project(cloudini_lib VERSION 1.2.4) +project(cloudini_lib VERSION 1.3.1) + +# When another project embeds this one (add_subdirectory / FetchContent), build +# only the library: no forced build type, no ament packaging, no tests, tools, +# benchmarks, PCL or install rules unless the parent asks for them. +# (PROJECT_IS_TOP_LEVEL needs CMake 3.21.) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(CLOUDINI_IS_TOP_LEVEL ON) +else() + set(CLOUDINI_IS_TOP_LEVEL OFF) +endif() # Make installed executables relocatable: find the co-installed shared library # through an $ORIGIN-relative RPATH (bin/ -> ../lib). conda/conda-forge tooling @@ -13,15 +23,17 @@ else() endif() # If not specified, set the default build type to Release -if (NOT CMAKE_BUILD_TYPE) +if (CLOUDINI_IS_TOP_LEVEL AND NOT CMAKE_BUILD_TYPE) message(STATUS "No build type selected, default to Release") set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo" "MinSizeRel") endif() message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") -option(CLOUDINI_BUILD_TOOLS "Build Tools" ON) +option(CLOUDINI_BUILD_TOOLS "Build Tools" ${CLOUDINI_IS_TOP_LEVEL}) option(CLOUDINI_FORCE_VENDORED_DEPS "Force vendored dependencies" ON) +option(CLOUDINI_WITH_PCL "Build the PCL conversions when PCL is found" ${CLOUDINI_IS_TOP_LEVEL}) +option(CLOUDINI_INSTALL "Generate install rules" ${CLOUDINI_IS_TOP_LEVEL}) # create compile_commands.json @@ -33,7 +45,12 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # add cmake folder to path list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -find_package(ament_cmake QUIET) +if(CLOUDINI_IS_TOP_LEVEL) + find_package(ament_cmake QUIET) +else() + # A ROS parent has found ament_cmake already; embedded, we are not a package. + set(ament_cmake_FOUND FALSE) +endif() if(ament_cmake_FOUND) message(STATUS "Found ament_cmake") set(CLOUDINI_FORCE_VENDORED_DEPS OFF) @@ -52,13 +69,15 @@ find_or_download_lz4(${CLOUDINI_FORCE_VENDORED_DEPS}) # ---- Trick to pass an absolute path to C++ code ---- get_filename_component(DATA_PATH "${CMAKE_CURRENT_LIST_DIR}/samples" ABSOLUTE) -configure_file (data_path.hpp.in "${CMAKE_BINARY_DIR}/include/data_path.hpp" @ONLY) +configure_file (data_path.hpp.in "${CMAKE_CURRENT_BINARY_DIR}/include/data_path.hpp" @ONLY) add_library(data_path INTERFACE IMPORTED) -target_include_directories(data_path INTERFACE ${CMAKE_BINARY_DIR}/include) +target_include_directories(data_path INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/include) ################################################################################## -find_package(PCL COMPONENTS common io QUIET) +if(CLOUDINI_WITH_PCL) + find_package(PCL COMPONENTS common io QUIET) +endif() if(PCL_FOUND) message(STATUS "PCL found in system") @@ -79,7 +98,7 @@ endif() # endif() # endif() -option(CLOUDINI_BUILD_BENCHMARKS "Build benchmarks" ON) +option(CLOUDINI_BUILD_BENCHMARKS "Build benchmarks" ${CLOUDINI_IS_TOP_LEVEL}) # Use SHARED library when building with ament, STATIC otherwise SET(CLOUDINI_SRC @@ -107,6 +126,10 @@ else() ) endif() +# Same name as the installed (non-ament) target, so consumers link +# cloudini::cloudini_lib whether Cloudini is installed or embedded. +add_library(cloudini::cloudini_lib ALIAS cloudini_lib) + target_compile_features(cloudini_lib PUBLIC cxx_std_20) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -136,11 +159,17 @@ else() set(CLOUDINI_LZ4_TARGET LZ4::lz4_static) endif() +# PointcloudEncoder uses std::thread. PUBLIC: a static cloudini_lib leaves the +# pthread references for whoever links the final binary. +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + target_link_libraries(cloudini_lib PRIVATE $ $ PUBLIC + Threads::Threads ${PCL_COMMON_LIBRARIES} ${PCL_IO_LIBRARIES} ) @@ -157,10 +186,12 @@ if(NOT EMSCRIPTEN) add_subdirectory(benchmarks) endif() - include(CTest) - message( STATUS "BUILD_TESTING: ${BUILD_TESTING} " ) - if (BUILD_TESTING) - add_subdirectory(test) + if(CLOUDINI_IS_TOP_LEVEL) # include(CTest) would define BUILD_TESTING in the parent's cache + include(CTest) + message( STATUS "BUILD_TESTING: ${BUILD_TESTING} " ) + if (BUILD_TESTING) + add_subdirectory(test) + endif() endif() else() @@ -215,69 +246,71 @@ if(CLOUDINI_BUILD_TOOLS AND NOT ament_cmake_FOUND) endif() endif() -INSTALL(TARGETS cloudini_lib - EXPORT cloudini_libTargets - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin - INCLUDES DESTINATION include - ) +if(CLOUDINI_INSTALL) + INSTALL(TARGETS cloudini_lib + EXPORT cloudini_libTargets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES DESTINATION include + ) -INSTALL( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ - DESTINATION include - FILES_MATCHING PATTERN "*.h*") + INSTALL( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ + DESTINATION include + FILES_MATCHING PATTERN "*.h*") -if(ament_cmake_FOUND) - ament_export_targets(cloudini_libTargets HAS_LIBRARY_TARGET) - ament_export_dependencies(PCL) - ament_package() -else() - # Standalone (non-ament) install: generate a CMake package config so downstream - # projects can do find_package(cloudini_lib) and link cloudini::cloudini_lib. - # Static archives retain compressor references: ship vendored archives or - # rediscover system libraries in the installed package config. - include(CMakePackageConfigHelpers) - - set(CLOUDINI_SYSTEM_LIBRARIES "") - if(_cloudini_lib_type STREQUAL "STATIC_LIBRARY") - foreach(_dependency IN ITEMS lz4 zstd) - set(_vendored_target "${_dependency}_static") - if(_dependency STREQUAL "zstd") - set(_vendored_target libzstd_static) - endif() - if(TARGET ${_vendored_target}) - set_target_properties(${_vendored_target} PROPERTIES OUTPUT_NAME cloudini_${_dependency}) - install(TARGETS ${_vendored_target} EXPORT cloudini_libTargets ARCHIVE DESTINATION lib) - target_link_libraries(cloudini_lib INTERFACE - $>) - else() - list(APPEND CLOUDINI_SYSTEM_LIBRARIES ${_dependency}) - endif() - endforeach() - endif() + if(ament_cmake_FOUND) + ament_export_targets(cloudini_libTargets HAS_LIBRARY_TARGET) + ament_export_dependencies(PCL Threads) + ament_package() + else() + # Standalone (non-ament) install: generate a CMake package config so downstream + # projects can do find_package(cloudini_lib) and link cloudini::cloudini_lib. + # Static archives retain compressor references: ship vendored archives or + # rediscover system libraries in the installed package config. + include(CMakePackageConfigHelpers) + + set(CLOUDINI_SYSTEM_LIBRARIES "") + if(_cloudini_lib_type STREQUAL "STATIC_LIBRARY") + foreach(_dependency IN ITEMS lz4 zstd) + set(_vendored_target "${_dependency}_static") + if(_dependency STREQUAL "zstd") + set(_vendored_target libzstd_static) + endif() + if(TARGET ${_vendored_target}) + set_target_properties(${_vendored_target} PROPERTIES OUTPUT_NAME cloudini_${_dependency}) + install(TARGETS ${_vendored_target} EXPORT cloudini_libTargets ARCHIVE DESTINATION lib) + target_link_libraries(cloudini_lib INTERFACE + $>) + else() + list(APPEND CLOUDINI_SYSTEM_LIBRARIES ${_dependency}) + endif() + endforeach() + endif() - install(EXPORT cloudini_libTargets - FILE cloudini_libTargets.cmake - NAMESPACE cloudini:: - DESTINATION lib/cmake/cloudini_lib) + install(EXPORT cloudini_libTargets + FILE cloudini_libTargets.cmake + NAMESPACE cloudini:: + DESTINATION lib/cmake/cloudini_lib) - set(CLOUDINI_HAS_PCL 0) - if(PCL_FOUND) - set(CLOUDINI_HAS_PCL 1) - endif() + set(CLOUDINI_HAS_PCL 0) + if(PCL_FOUND) + set(CLOUDINI_HAS_PCL 1) + endif() - configure_package_config_file( - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/cloudini_libConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake - INSTALL_DESTINATION lib/cmake/cloudini_lib) + configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/cloudini_libConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake + INSTALL_DESTINATION lib/cmake/cloudini_lib) - write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion) + write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) - install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake - DESTINATION lib/cmake/cloudini_lib) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake + DESTINATION lib/cmake/cloudini_lib) + endif() endif() diff --git a/cloudini_lib/cmake/cloudini_libConfig.cmake.in b/cloudini_lib/cmake/cloudini_libConfig.cmake.in index e9947d6..e3d4b04 100644 --- a/cloudini_lib/cmake/cloudini_libConfig.cmake.in +++ b/cloudini_lib/cmake/cloudini_libConfig.cmake.in @@ -2,6 +2,9 @@ include(CMakeFindDependencyMacro) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_dependency(Threads) + # PCL only leaks into the interface when cloudini_lib was built with PCL support. if(@CLOUDINI_HAS_PCL@) find_dependency(PCL COMPONENTS common io) diff --git a/cloudini_lib/cmake/find_or_download_lz4.cmake b/cloudini_lib/cmake/find_or_download_lz4.cmake index ca061e8..356ba8a 100644 --- a/cloudini_lib/cmake/find_or_download_lz4.cmake +++ b/cloudini_lib/cmake/find_or_download_lz4.cmake @@ -1,9 +1,16 @@ function(find_or_download_lz4 FORCE_VENDORED) if(NOT FORCE_VENDORED) - find_package(lz4 CONFIG QUIET) - if(NOT TARGET LZ4::lz4_static) - find_package(LZ4 QUIET) + # Same as for zstd: reuse targets a parent project already defined. + if(NOT TARGET LZ4::lz4_static AND NOT TARGET LZ4::lz4_shared) + find_package(lz4 CONFIG QUIET) + if(NOT TARGET LZ4::lz4_static) + find_package(LZ4 QUIET) + endif() + endif() + if(NOT TARGET LZ4::lz4_static AND TARGET LZ4::lz4_shared) + add_library(LZ4::lz4_static INTERFACE IMPORTED) + set_target_properties(LZ4::lz4_static PROPERTIES INTERFACE_LINK_LIBRARIES LZ4::lz4_shared) endif() # Fallback for systems without cmake config (e.g. Ubuntu Jammy liblz4-dev) if(NOT TARGET LZ4::lz4_static) diff --git a/cloudini_lib/cmake/find_or_download_zstd.cmake b/cloudini_lib/cmake/find_or_download_zstd.cmake index 004b79e..7dbdd67 100644 --- a/cloudini_lib/cmake/find_or_download_zstd.cmake +++ b/cloudini_lib/cmake/find_or_download_zstd.cmake @@ -1,9 +1,14 @@ function(find_or_download_zstd FORCE_VENDORED) if(NOT FORCE_VENDORED) - find_package(zstd CONFIG QUIET) - if(NOT TARGET zstd::libzstd_static) - find_package(ZSTD QUIET) + # A parent project may have defined (some of) these targets already, e.g. from + # a Find module. Looking zstd up again would then fail with "some (but not all) + # targets in this export set were already defined": reuse what is there. + if(NOT TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared AND NOT TARGET zstd::libzstd) + find_package(zstd CONFIG QUIET) + if(NOT TARGET zstd::libzstd_static) + find_package(ZSTD QUIET) + endif() endif() # Normalize target names. This project links zstd::libzstd_static, but some diff --git a/cloudini_lib/package.xml b/cloudini_lib/package.xml index 352d135..46fa3cb 100644 --- a/cloudini_lib/package.xml +++ b/cloudini_lib/package.xml @@ -1,7 +1,7 @@ cloudini_lib - 1.3.0 + 1.3.1 Main library of Cloudini, the pointcloud compression library diff --git a/cloudini_lib/test/subproject/CMakeLists.txt b/cloudini_lib/test/subproject/CMakeLists.txt new file mode 100644 index 0000000..967c54f --- /dev/null +++ b/cloudini_lib/test/subproject/CMakeLists.txt @@ -0,0 +1,48 @@ +# Builds cloudini_lib the way a parent project embeds it (add_subdirectory or +# FetchContent) and fails at configure time if Cloudini leaks into the parent. +# cmake -S cloudini_lib/test/subproject -B build_subproject && cmake --build build_subproject +cmake_minimum_required(VERSION 3.16) +project(cloudini_subproject_consumer LANGUAGES C CXX) + +# A parent that already provides zstd under the conventional target names, as a +# Find module does. Cloudini must reuse it instead of looking zstd up again. +find_library(CONSUMER_ZSTD_LIBRARY NAMES zstd REQUIRED) +find_path(CONSUMER_ZSTD_INCLUDE_DIR NAMES zstd.h REQUIRED) +add_library(zstd::libzstd_shared SHARED IMPORTED GLOBAL) +set_target_properties(zstd::libzstd_shared PROPERTIES + IMPORTED_LOCATION "${CONSUMER_ZSTD_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${CONSUMER_ZSTD_INCLUDE_DIR}") + +# A ROS parent has already found ament_cmake; that must not turn the embedded +# Cloudini into a second ament package. +find_package(ament_cmake QUIET) + +set(CLOUDINI_FORCE_VENDORED_DEPS OFF CACHE BOOL "" FORCE) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. ${CMAKE_CURRENT_BINARY_DIR}/cloudini_lib) + +if(NOT TARGET cloudini::cloudini_lib) + message(FATAL_ERROR "embedded Cloudini does not define cloudini::cloudini_lib") +endif() +get_target_property(_type cloudini_lib TYPE) +if(NOT _type STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR "embedded Cloudini is ${_type}, expected STATIC_LIBRARY") +endif() +if(CMAKE_BUILD_TYPE) + message(FATAL_ERROR "embedded Cloudini forced CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} on the parent") +endif() +foreach(_leaked test_cloudini cloudini_rosbag_converter) + if(TARGET ${_leaked}) + message(FATAL_ERROR "embedded Cloudini built '${_leaked}' inside the parent") + endif() +endforeach() +get_target_property(_links cloudini_lib INTERFACE_LINK_LIBRARIES) +if(_links MATCHES "pcl|PCL") + message(FATAL_ERROR "embedded Cloudini made PCL a link dependency of the parent: ${_links}") +endif() +if(EXISTS "${CMAKE_BINARY_DIR}/include/data_path.hpp") + message(FATAL_ERROR "embedded Cloudini wrote data_path.hpp into the parent's include directory") +endif() + +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE cloudini::cloudini_lib) +install(TARGETS consumer DESTINATION bin) diff --git a/cloudini_lib/test/subproject/main.cpp b/cloudini_lib/test/subproject/main.cpp new file mode 100644 index 0000000..001b40d --- /dev/null +++ b/cloudini_lib/test/subproject/main.cpp @@ -0,0 +1,9 @@ +#include "cloudini_lib/cloudini.hpp" + +int main() { + Cloudini::EncodingInfo info; + info.fields = {{"x", 0, Cloudini::FieldType::FLOAT32, 0.001f}}; + info.point_step = 4; + Cloudini::PointcloudEncoder encoder(info); + return 0; +} diff --git a/cloudini_ros/CHANGELOG.rst b/cloudini_ros/CHANGELOG.rst index 69af02c..10d30a9 100644 --- a/cloudini_ros/CHANGELOG.rst +++ b/cloudini_ros/CHANGELOG.rst @@ -2,6 +2,10 @@ Changelog for package cloudini_ros ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1.3.1 (2026-09-20) +------------------ +* No changes; released together with cloudini_lib 1.3.1 + 1.3.0 (2026-09-20) ------------------ * Fix license tags in package.xml diff --git a/cloudini_ros/package.xml b/cloudini_ros/package.xml index 49578cd..4e092f7 100644 --- a/cloudini_ros/package.xml +++ b/cloudini_ros/package.xml @@ -1,7 +1,7 @@ cloudini_ros - 1.3.0 + 1.3.1 Main library of Cloudini, the pointcloud compression library diff --git a/conda/recipe.yaml b/conda/recipe.yaml index abb029c..dbacd1d 100644 --- a/conda/recipe.yaml +++ b/conda/recipe.yaml @@ -9,7 +9,7 @@ # curl -sL https://github.com/facontidavide/cloudini/archive/refs/tags/${version}.tar.gz | sha256sum context: - version: "1.2.4" + version: "1.3.1" package: name: cloudini @@ -17,6 +17,7 @@ package: source: - url: https://github.com/facontidavide/cloudini/archive/refs/tags/${{ version }}.tar.gz + # STALE: still the 1.2.4 tarball. Refresh after tagging 1.3.1 (command above). sha256: 6a815ff0418115fc23d0f8a8299c20904cdf3cd3bf0a5227c23d2c2269e7943d # mcap is a header-only dependency of the CLI and is not packaged for C++ on # conda-forge; fetch it here (network is available during the source stage but