Skip to content
Open
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
62 changes: 57 additions & 5 deletions cloudini_lib/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -108,17 +119,26 @@ target_include_directories(cloudini_lib
$<INSTALL_INTERFACE:include>
)

# 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
$<BUILD_INTERFACE:LZ4::lz4_static>
$<BUILD_INTERFACE:${CLOUDINI_LZ4_TARGET}>
$<BUILD_INTERFACE:${CLOUDINI_ZSTD_TARGET}>
PUBLIC
${PCL_COMMON_LIBRARIES}
Expand Down Expand Up @@ -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()
12 changes: 12 additions & 0 deletions cloudini_lib/cmake/cloudini_libConfig.cmake.in
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions cloudini_lib/cmake/find_or_download_mcap.cmake
Original file line number Diff line number Diff line change
@@ -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=<checkout>/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)
Expand Down
16 changes: 16 additions & 0 deletions cloudini_lib/cmake/find_or_download_zstd.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion cloudini_lib/tools/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Expand Down
97 changes: 97 additions & 0 deletions conda/RELEASING.md
Original file line number Diff line number Diff line change
@@ -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 <YOUR_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` → `<version>` (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`
94 changes: 94 additions & 0 deletions conda/recipe.yaml
Original file line number Diff line number Diff line change
@@ -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