diff --git a/cloudini_lib/CMakeLists.txt b/cloudini_lib/CMakeLists.txt index a38c1ad..ff8d591 100644 --- a/cloudini_lib/CMakeLists.txt +++ b/cloudini_lib/CMakeLists.txt @@ -1,5 +1,16 @@ cmake_minimum_required(VERSION 3.16) -project(cloudini_lib) +# 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) + +# Make installed executables relocatable: find the co-installed shared library +# through an $ORIGIN-relative RPATH (bin/ -> ../lib). conda/conda-forge tooling +# manages RPATHs on its own, so this mainly benefits a plain `cmake --install`. +if(APPLE) + set(CMAKE_INSTALL_RPATH "@loader_path/../lib") +else() + set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") +endif() # If not specified, set the default build type to Release if (NOT CMAKE_BUILD_TYPE) @@ -108,17 +119,26 @@ target_include_directories(cloudini_lib $ ) -# In ament/ROS builds cloudini_lib is a shared library. System libzstd.a is not -# compiled with -fPIC and cannot be embedded in a .so; use the shared variant. -if(ament_cmake_FOUND AND TARGET zstd::libzstd_shared) +# When cloudini_lib is a shared library (ROS/ament, or a standalone build with +# BUILD_SHARED_LIBS=ON such as the conda package), prefer the *shared* variants +# of zstd/lz4 when available: a non-PIC system libzstd.a / liblz4.a cannot be +# embedded into a .so. Vendored builds only expose the *_static targets, which +# are used as the fallback. +get_target_property(_cloudini_lib_type cloudini_lib TYPE) +if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET zstd::libzstd_shared) set(CLOUDINI_ZSTD_TARGET zstd::libzstd_shared) else() set(CLOUDINI_ZSTD_TARGET zstd::libzstd_static) endif() +if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET LZ4::lz4_shared) + set(CLOUDINI_LZ4_TARGET LZ4::lz4_shared) +else() + set(CLOUDINI_LZ4_TARGET LZ4::lz4_static) +endif() target_link_libraries(cloudini_lib PRIVATE - $ + $ $ PUBLIC ${PCL_COMMON_LIBRARIES} @@ -211,4 +231,36 @@ 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. + # This is what the conda / system packages ship. zstd/lz4 are linked PRIVATE + + # BUILD_INTERFACE, so they never leak into the exported interface; PCL does when + # enabled, hence the conditional find_dependency in the config template. + include(CMakePackageConfigHelpers) + + 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() + + 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) + + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake + DESTINATION lib/cmake/cloudini_lib) endif() diff --git a/cloudini_lib/cmake/cloudini_libConfig.cmake.in b/cloudini_lib/cmake/cloudini_libConfig.cmake.in new file mode 100644 index 0000000..c85d5c2 --- /dev/null +++ b/cloudini_lib/cmake/cloudini_libConfig.cmake.in @@ -0,0 +1,12 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# PCL only leaks into the interface when cloudini_lib was built with PCL support. +if(@CLOUDINI_HAS_PCL@) + find_dependency(PCL COMPONENTS common io) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/cloudini_libTargets.cmake") + +check_required_components(cloudini_lib) diff --git a/cloudini_lib/cmake/find_or_download_mcap.cmake b/cloudini_lib/cmake/find_or_download_mcap.cmake index 5ba22f6..1f60abf 100644 --- a/cloudini_lib/cmake/find_or_download_mcap.cmake +++ b/cloudini_lib/cmake/find_or_download_mcap.cmake @@ -1,5 +1,16 @@ function(find_or_download_mcap) + # Offline / pre-fetched header path. mcap is header-only and not packaged for + # C++ on some channels (e.g. conda-forge), whose build sandboxes also have no + # network. Point -DMCAP_INCLUDE_DIR=/cpp/mcap/include at a source + # fetched ahead of time and we skip the download entirely. + if(NOT TARGET mcap AND MCAP_INCLUDE_DIR) + add_library(mcap INTERFACE) + target_include_directories(mcap INTERFACE "${MCAP_INCLUDE_DIR}") + add_library(mcap::mcap ALIAS mcap) + return() + endif() + # Try system mcap_vendor first (installed on ROS buildfarm via package.xml build_depend) if(NOT TARGET mcap AND NOT TARGET mcap_vendor::mcap) find_package(mcap_vendor QUIET) diff --git a/cloudini_lib/cmake/find_or_download_zstd.cmake b/cloudini_lib/cmake/find_or_download_zstd.cmake index 534d16e..b4c7f3c 100644 --- a/cloudini_lib/cmake/find_or_download_zstd.cmake +++ b/cloudini_lib/cmake/find_or_download_zstd.cmake @@ -5,6 +5,22 @@ function(find_or_download_zstd FORCE_VENDORED) if(NOT TARGET zstd::libzstd_static) find_package(ZSTD QUIET) endif() + + # Normalize target names. This project links zstd::libzstd_static, but some + # packagings expose only zstd::libzstd_shared / zstd::libzstd (notably + # conda-forge, which ships no static zstd). Alias whatever was found to the + # expected name so we use the system library instead of vendoring a copy. + if(NOT TARGET zstd::libzstd_static) + foreach(_zstd_found zstd::libzstd_shared zstd::libzstd) + if(TARGET ${_zstd_found}) + add_library(zstd::libzstd_static INTERFACE IMPORTED) + set_target_properties(zstd::libzstd_static PROPERTIES + INTERFACE_LINK_LIBRARIES ${_zstd_found}) + break() + endif() + endforeach() + endif() + if(TARGET zstd::libzstd_static) return() endif() diff --git a/cloudini_lib/tools/CMakeLists.txt b/cloudini_lib/tools/CMakeLists.txt index a08755f..0496fc7 100644 --- a/cloudini_lib/tools/CMakeLists.txt +++ b/cloudini_lib/tools/CMakeLists.txt @@ -1,6 +1,11 @@ -add_library(mcap_converter src/mcap_converter.cpp) +# STATIC on purpose: this is an internal helper linked into the CLI executables. +# With BUILD_SHARED_LIBS=ON (e.g. the conda package) an unqualified add_library +# would produce a libmcap_converter.so that is never installed, leaving the CLI +# with a dangling runtime dependency. +add_library(mcap_converter STATIC src/mcap_converter.cpp) +set_property(TARGET mcap_converter PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(mcap_converter PUBLIC $ diff --git a/conda/RELEASING.md b/conda/RELEASING.md new file mode 100644 index 0000000..1ea0d97 --- /dev/null +++ b/conda/RELEASING.md @@ -0,0 +1,97 @@ +# Releasing Cloudini as a conda package (pixi / prefix.dev + conda-forge) + +`conda/recipe.yaml` builds a conda package that ships: + +- `libcloudini_lib.so` (shared) + headers + a CMake package config + (`find_package(cloudini_lib)` → `cloudini::cloudini_lib`) +- the `cloudini_rosbag_converter` CLI + +The build is **hermetic**: it links conda's `zstd` / `lz4-c`, uses conda's +`cxxopts`, and consumes `mcap` from a pre-fetched source (no network during the +build script). The same recipe is used for both the prefix.dev channel and the +conda-forge submission. + +## Prerequisites (one time) + +```bash +pixi global install rattler-build # already installed here +rattler-build auth login https://prefix.dev --token +# token: prefix.dev → Settings → API tokens (needs write to the 'cloudini' channel) +``` + +## Cut a release + +1. **Pick the version** and make it consistent in three places (they must equal + the git tag): + - `cloudini_lib/CMakeLists.txt` → `project(cloudini_lib VERSION X.Y.Z)` + - `conda/recipe.yaml` → `context.version` + - (optional) `cloudini_lib/package.xml` → `` (ROS manifest) + +2. **Commit, tag, push:** + ```bash + git commit -am "release: X.Y.Z" + git tag X.Y.Z + git push origin main --tags + ``` + +3. **Fill the source hash** in `conda/recipe.yaml`: + ```bash + curl -sL https://github.com/facontidavide/cloudini/archive/refs/tags/X.Y.Z.tar.gz \ + | sha256sum + # paste into source[0].sha256 + ``` + +## Publish to your prefix.dev channel + +```bash +# Build + upload + index in one step: +rattler-build publish ./conda/recipe.yaml --to https://prefix.dev/cloudini + +# …or in two steps: +rattler-build build --recipe conda/recipe.yaml -c conda-forge +rattler-build upload prefix -c cloudini ./output/**/*.conda +``` + +Verify from a clean env: + +```bash +pixi init /tmp/cloudini-check && cd /tmp/cloudini-check +pixi project channel add https://prefix.dev/cloudini +pixi project channel add conda-forge +pixi add cloudini +pixi run cloudini_rosbag_converter --help +``` + +## Submit to conda-forge (broad reach: `pixi add cloudini` with no extra channel) + +1. Fork `conda-forge/staged-recipes`. +2. Copy `conda/recipe.yaml` to `recipes/cloudini/recipe.yaml` in that fork + (it is already conda-forge-compatible: pinned tarball + sha256, + `extra.recipe-maintainers`, hermetic build, run_exports-driven run deps). +3. Open a PR. conda-forge CI builds linux-64/osx-64/osx-arm64. Once merged, a + `cloudini-feedstock` repo is created for you to maintain; a bot opens version + bump PRs automatically thereafter. + +Notes for the conda-forge review: +- Windows is not enabled (POSIX-oriented CLI + `-msse4.1`); add a `bld.bat` + + `skip` logic later if desired. +- PCL is intentionally not a dependency, so `pcl_conversion.hpp` ships but is + only usable by consumers that bring their own PCL. + +## Local dry-run without a tag + +Build straight from a checkout (no tag/sha256 needed) using a `path:` source — +see the throwaway recipe used during bring-up. Handy for testing recipe/CMake +changes before cutting a tag. +``` + +## What the packaging touched in the library + +To make the library installable/consumable outside ament, this release added to +`cloudini_lib/CMakeLists.txt` and its cmake modules: +- `install(EXPORT ...)` + generated `cloudini_libConfig.cmake` for non-ament builds +- `$ORIGIN/../lib` install RPATH so installed executables find the co-installed lib +- shared-variant zstd/lz4 selection when the library is shared +- `find_or_download_zstd.cmake`: accept conda's `zstd::libzstd_shared` / `zstd::libzstd` +- `find_or_download_mcap.cmake`: `-DMCAP_INCLUDE_DIR=` offline hook +- `mcap_converter` pinned `STATIC` so the CLI stays self-contained under `BUILD_SHARED_LIBS=ON` diff --git a/conda/recipe.yaml b/conda/recipe.yaml new file mode 100644 index 0000000..ed8a8ad --- /dev/null +++ b/conda/recipe.yaml @@ -0,0 +1,94 @@ +# rattler-build recipe for the Cloudini conda package (library + CLI). +# +# Build locally: rattler-build build --recipe conda/recipe.yaml +# Publish: rattler-build upload prefix -c cloudini output/**/*.conda +# (or) rattler-build publish ./conda/recipe.yaml --to https://prefix.dev/cloudini +# +# The same file is used for the conda-forge/staged-recipes submission. +# After cutting the git tag, fill in `sha256` with: +# curl -sL https://github.com/facontidavide/cloudini/archive/refs/tags/${version}.tar.gz | sha256sum + +context: + version: "1.2.4" + +package: + name: cloudini + version: ${{ version }} + +source: + - url: https://github.com/facontidavide/cloudini/archive/refs/tags/${{ version }}.tar.gz + 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 + # NOT during the build script) and feed it in via -DMCAP_INCLUDE_DIR below. + - url: https://github.com/foxglove/mcap/archive/refs/tags/releases/cpp/v2.0.2.tar.gz + sha256: af44f92b62f43bd4b44a00728780f93df59601537d9aebbc862c4cb9b5ad743e + target_directory: mcap-src + +build: + number: 0 + script: + # Locate the pre-fetched mcap headers regardless of the archive's top dir. + - MCAP_INCLUDE_DIR="$(find "$PWD/mcap-src" -type d -path '*/cpp/mcap/include' | head -1)" + - cmake ${CMAKE_ARGS} -GNinja -B build -S cloudini_lib + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=ON + -DCLOUDINI_FORCE_VENDORED_DEPS=OFF + -DCLOUDINI_BUILD_BENCHMARKS=OFF + -DBUILD_TESTING=OFF + -DMCAP_INCLUDE_DIR="${MCAP_INCLUDE_DIR}" + -DCMAKE_INSTALL_PREFIX=$PREFIX + - cmake --build build --parallel ${CPU_COUNT} + - cmake --install build + +requirements: + build: + - ${{ compiler('c') }} + - ${{ compiler('cxx') }} + - cmake + - ninja + host: + # zstd and lz4-c both declare run_exports, so their runtime dependency is + # added automatically with the correct version pin — no `run:` entries needed. + - zstd + - lz4-c + # cxxopts is header-only (CLI arg parsing); found via find_package(cxxopts). + - cxxopts + +tests: + # Assert the package actually shipped the library, headers, CMake config, and CLI. + - package_contents: + include: + - cloudini_lib/cloudini.hpp + lib: + - cloudini_lib + bin: + - cloudini_rosbag_converter + files: + - lib/cmake/cloudini_lib/cloudini_libConfig.cmake + - lib/cmake/cloudini_lib/cloudini_libTargets.cmake + # The CLI binary must load its shared library and start up. + - script: + - cloudini_rosbag_converter --help + +about: + homepage: https://github.com/facontidavide/cloudini + repository: https://github.com/facontidavide/cloudini + documentation: https://github.com/facontidavide/cloudini#readme + license: Apache-2.0 + license_file: LICENSE + summary: High-performance pointcloud compression library and CLI + description: | + Cloudini is a high-performance pointcloud compression library focused on + speed while still achieving very good compression ratios. It uses a + two-stage approach: custom per-field encoding followed by general-purpose + compression (LZ4/ZSTD). Typical use cases are shrinking rosbag/MCAP datasets + that contain pointclouds and reducing bandwidth when streaming pointclouds + over a network. This package ships the C++ library (with a CMake package + config, `find_package(cloudini_lib)` + `cloudini::cloudini_lib`) and the + `cloudini_rosbag_converter` command-line tool for compressing/decompressing + MCAP rosbags. + +extra: + recipe-maintainers: + - facontidavide