Skip to content

Commit 3ced7d5

Browse files
facontidavideclaude
andcommitted
feat(packaging): add conda/pixi package (prefix.dev + conda-forge)
Ship cloudini as a conda package (shared library + headers + CMake package config + cloudini_rosbag_converter CLI), buildable with rattler-build and publishable to prefix.dev and conda-forge. Library/CMake changes to make it installable outside ament: - install(EXPORT) + generated cloudini_libConfig.cmake for standalone builds, so downstream can find_package(cloudini_lib) and link cloudini::cloudini_lib - $ORIGIN/../lib install RPATH so installed executables locate the lib - prefer shared zstd/lz4 variants when the library itself is shared - find_or_download_zstd.cmake: accept conda's zstd::libzstd_shared / zstd::libzstd (conda-forge ships no static zstd) instead of vendoring a copy - find_or_download_mcap.cmake: -DMCAP_INCLUDE_DIR offline hook (no-network builds) - pin mcap_converter STATIC so the CLI stays self-contained under BUILD_SHARED_LIBS=ON conda/recipe.yaml is hermetic (no network at build): links conda zstd/lz4-c, uses conda cxxopts, and consumes a pre-fetched mcap source. Same recipe serves prefix.dev and conda-forge/staged-recipes. conda/RELEASING.md documents the release + publish workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d202e52 commit 3ced7d5

7 files changed

Lines changed: 293 additions & 6 deletions

File tree

cloudini_lib/CMakeLists.txt

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
cmake_minimum_required(VERSION 3.16)
2-
project(cloudini_lib)
2+
# NOTE: keep the default languages (C and CXX). The vendored zstd/lz4 fallback
3+
# compiles .c sources, so restricting to CXX only would break standalone builds.
4+
project(cloudini_lib VERSION 1.2.4)
5+
6+
# Make installed executables relocatable: find the co-installed shared library
7+
# through an $ORIGIN-relative RPATH (bin/ -> ../lib). conda/conda-forge tooling
8+
# manages RPATHs on its own, so this mainly benefits a plain `cmake --install`.
9+
if(APPLE)
10+
set(CMAKE_INSTALL_RPATH "@loader_path/../lib")
11+
else()
12+
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
13+
endif()
314

415
# If not specified, set the default build type to Release
516
if (NOT CMAKE_BUILD_TYPE)
@@ -108,17 +119,26 @@ target_include_directories(cloudini_lib
108119
$<INSTALL_INTERFACE:include>
109120
)
110121

111-
# In ament/ROS builds cloudini_lib is a shared library. System libzstd.a is not
112-
# compiled with -fPIC and cannot be embedded in a .so; use the shared variant.
113-
if(ament_cmake_FOUND AND TARGET zstd::libzstd_shared)
122+
# When cloudini_lib is a shared library (ROS/ament, or a standalone build with
123+
# BUILD_SHARED_LIBS=ON such as the conda package), prefer the *shared* variants
124+
# of zstd/lz4 when available: a non-PIC system libzstd.a / liblz4.a cannot be
125+
# embedded into a .so. Vendored builds only expose the *_static targets, which
126+
# are used as the fallback.
127+
get_target_property(_cloudini_lib_type cloudini_lib TYPE)
128+
if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET zstd::libzstd_shared)
114129
set(CLOUDINI_ZSTD_TARGET zstd::libzstd_shared)
115130
else()
116131
set(CLOUDINI_ZSTD_TARGET zstd::libzstd_static)
117132
endif()
133+
if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET LZ4::lz4_shared)
134+
set(CLOUDINI_LZ4_TARGET LZ4::lz4_shared)
135+
else()
136+
set(CLOUDINI_LZ4_TARGET LZ4::lz4_static)
137+
endif()
118138

119139
target_link_libraries(cloudini_lib
120140
PRIVATE
121-
$<BUILD_INTERFACE:LZ4::lz4_static>
141+
$<BUILD_INTERFACE:${CLOUDINI_LZ4_TARGET}>
122142
$<BUILD_INTERFACE:${CLOUDINI_ZSTD_TARGET}>
123143
PUBLIC
124144
${PCL_COMMON_LIBRARIES}
@@ -211,4 +231,36 @@ if(ament_cmake_FOUND)
211231
ament_export_targets(cloudini_libTargets HAS_LIBRARY_TARGET)
212232
ament_export_dependencies(PCL)
213233
ament_package()
234+
else()
235+
# Standalone (non-ament) install: generate a CMake package config so downstream
236+
# projects can do find_package(cloudini_lib) and link cloudini::cloudini_lib.
237+
# This is what the conda / system packages ship. zstd/lz4 are linked PRIVATE +
238+
# BUILD_INTERFACE, so they never leak into the exported interface; PCL does when
239+
# enabled, hence the conditional find_dependency in the config template.
240+
include(CMakePackageConfigHelpers)
241+
242+
install(EXPORT cloudini_libTargets
243+
FILE cloudini_libTargets.cmake
244+
NAMESPACE cloudini::
245+
DESTINATION lib/cmake/cloudini_lib)
246+
247+
set(CLOUDINI_HAS_PCL 0)
248+
if(PCL_FOUND)
249+
set(CLOUDINI_HAS_PCL 1)
250+
endif()
251+
252+
configure_package_config_file(
253+
${CMAKE_CURRENT_SOURCE_DIR}/cmake/cloudini_libConfig.cmake.in
254+
${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake
255+
INSTALL_DESTINATION lib/cmake/cloudini_lib)
256+
257+
write_basic_package_version_file(
258+
${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake
259+
VERSION ${PROJECT_VERSION}
260+
COMPATIBILITY SameMajorVersion)
261+
262+
install(FILES
263+
${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake
264+
${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake
265+
DESTINATION lib/cmake/cloudini_lib)
214266
endif()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@PACKAGE_INIT@
2+
3+
include(CMakeFindDependencyMacro)
4+
5+
# PCL only leaks into the interface when cloudini_lib was built with PCL support.
6+
if(@CLOUDINI_HAS_PCL@)
7+
find_dependency(PCL COMPONENTS common io)
8+
endif()
9+
10+
include("${CMAKE_CURRENT_LIST_DIR}/cloudini_libTargets.cmake")
11+
12+
check_required_components(cloudini_lib)

cloudini_lib/cmake/find_or_download_mcap.cmake

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
function(find_or_download_mcap)
22

3+
# Offline / pre-fetched header path. mcap is header-only and not packaged for
4+
# C++ on some channels (e.g. conda-forge), whose build sandboxes also have no
5+
# network. Point -DMCAP_INCLUDE_DIR=<checkout>/cpp/mcap/include at a source
6+
# fetched ahead of time and we skip the download entirely.
7+
if(NOT TARGET mcap AND MCAP_INCLUDE_DIR)
8+
add_library(mcap INTERFACE)
9+
target_include_directories(mcap INTERFACE "${MCAP_INCLUDE_DIR}")
10+
add_library(mcap::mcap ALIAS mcap)
11+
return()
12+
endif()
13+
314
# Try system mcap_vendor first (installed on ROS buildfarm via package.xml build_depend)
415
if(NOT TARGET mcap AND NOT TARGET mcap_vendor::mcap)
516
find_package(mcap_vendor QUIET)

cloudini_lib/cmake/find_or_download_zstd.cmake

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ function(find_or_download_zstd FORCE_VENDORED)
55
if(NOT TARGET zstd::libzstd_static)
66
find_package(ZSTD QUIET)
77
endif()
8+
9+
# Normalize target names. This project links zstd::libzstd_static, but some
10+
# packagings expose only zstd::libzstd_shared / zstd::libzstd (notably
11+
# conda-forge, which ships no static zstd). Alias whatever was found to the
12+
# expected name so we use the system library instead of vendoring a copy.
13+
if(NOT TARGET zstd::libzstd_static)
14+
foreach(_zstd_found zstd::libzstd_shared zstd::libzstd)
15+
if(TARGET ${_zstd_found})
16+
add_library(zstd::libzstd_static INTERFACE IMPORTED)
17+
set_target_properties(zstd::libzstd_static PROPERTIES
18+
INTERFACE_LINK_LIBRARIES ${_zstd_found})
19+
break()
20+
endif()
21+
endforeach()
22+
endif()
23+
824
if(TARGET zstd::libzstd_static)
925
return()
1026
endif()

cloudini_lib/tools/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11

22

3-
add_library(mcap_converter src/mcap_converter.cpp)
3+
# STATIC on purpose: this is an internal helper linked into the CLI executables.
4+
# With BUILD_SHARED_LIBS=ON (e.g. the conda package) an unqualified add_library
5+
# would produce a libmcap_converter.so that is never installed, leaving the CLI
6+
# with a dangling runtime dependency.
7+
add_library(mcap_converter STATIC src/mcap_converter.cpp)
8+
set_property(TARGET mcap_converter PROPERTY POSITION_INDEPENDENT_CODE ON)
49
target_include_directories(mcap_converter
510
PUBLIC
611
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>

conda/RELEASING.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Releasing Cloudini as a conda package (pixi / prefix.dev + conda-forge)
2+
3+
`conda/recipe.yaml` builds a conda package that ships:
4+
5+
- `libcloudini_lib.so` (shared) + headers + a CMake package config
6+
(`find_package(cloudini_lib)``cloudini::cloudini_lib`)
7+
- the `cloudini_rosbag_converter` CLI
8+
9+
The build is **hermetic**: it links conda's `zstd` / `lz4-c`, uses conda's
10+
`cxxopts`, and consumes `mcap` from a pre-fetched source (no network during the
11+
build script). The same recipe is used for both the prefix.dev channel and the
12+
conda-forge submission.
13+
14+
## Prerequisites (one time)
15+
16+
```bash
17+
pixi global install rattler-build # already installed here
18+
rattler-build auth login https://prefix.dev --token <YOUR_PREFIX_DEV_TOKEN>
19+
# token: prefix.dev → Settings → API tokens (needs write to the 'cloudini' channel)
20+
```
21+
22+
## Cut a release
23+
24+
1. **Pick the version** and make it consistent in three places (they must equal
25+
the git tag):
26+
- `cloudini_lib/CMakeLists.txt``project(cloudini_lib VERSION X.Y.Z)`
27+
- `conda/recipe.yaml``context.version`
28+
- (optional) `cloudini_lib/package.xml``<version>` (ROS manifest)
29+
30+
2. **Commit, tag, push:**
31+
```bash
32+
git commit -am "release: X.Y.Z"
33+
git tag X.Y.Z
34+
git push origin main --tags
35+
```
36+
37+
3. **Fill the source hash** in `conda/recipe.yaml`:
38+
```bash
39+
curl -sL https://github.com/facontidavide/cloudini/archive/refs/tags/X.Y.Z.tar.gz \
40+
| sha256sum
41+
# paste into source[0].sha256
42+
```
43+
44+
## Publish to your prefix.dev channel
45+
46+
```bash
47+
# Build + upload + index in one step:
48+
rattler-build publish ./conda/recipe.yaml --to https://prefix.dev/cloudini
49+
50+
# …or in two steps:
51+
rattler-build build --recipe conda/recipe.yaml -c conda-forge
52+
rattler-build upload prefix -c cloudini ./output/**/*.conda
53+
```
54+
55+
Verify from a clean env:
56+
57+
```bash
58+
pixi init /tmp/cloudini-check && cd /tmp/cloudini-check
59+
pixi project channel add https://prefix.dev/cloudini
60+
pixi project channel add conda-forge
61+
pixi add cloudini
62+
pixi run cloudini_rosbag_converter --help
63+
```
64+
65+
## Submit to conda-forge (broad reach: `pixi add cloudini` with no extra channel)
66+
67+
1. Fork `conda-forge/staged-recipes`.
68+
2. Copy `conda/recipe.yaml` to `recipes/cloudini/recipe.yaml` in that fork
69+
(it is already conda-forge-compatible: pinned tarball + sha256,
70+
`extra.recipe-maintainers`, hermetic build, run_exports-driven run deps).
71+
3. Open a PR. conda-forge CI builds linux-64/osx-64/osx-arm64. Once merged, a
72+
`cloudini-feedstock` repo is created for you to maintain; a bot opens version
73+
bump PRs automatically thereafter.
74+
75+
Notes for the conda-forge review:
76+
- Windows is not enabled (POSIX-oriented CLI + `-msse4.1`); add a `bld.bat` +
77+
`skip` logic later if desired.
78+
- PCL is intentionally not a dependency, so `pcl_conversion.hpp` ships but is
79+
only usable by consumers that bring their own PCL.
80+
81+
## Local dry-run without a tag
82+
83+
Build straight from a checkout (no tag/sha256 needed) using a `path:` source —
84+
see the throwaway recipe used during bring-up. Handy for testing recipe/CMake
85+
changes before cutting a tag.
86+
```
87+
88+
## What the packaging touched in the library
89+
90+
To make the library installable/consumable outside ament, this release added to
91+
`cloudini_lib/CMakeLists.txt` and its cmake modules:
92+
- `install(EXPORT ...)` + generated `cloudini_libConfig.cmake` for non-ament builds
93+
- `$ORIGIN/../lib` install RPATH so installed executables find the co-installed lib
94+
- shared-variant zstd/lz4 selection when the library is shared
95+
- `find_or_download_zstd.cmake`: accept conda's `zstd::libzstd_shared` / `zstd::libzstd`
96+
- `find_or_download_mcap.cmake`: `-DMCAP_INCLUDE_DIR=` offline hook
97+
- `mcap_converter` pinned `STATIC` so the CLI stays self-contained under `BUILD_SHARED_LIBS=ON`

conda/recipe.yaml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# rattler-build recipe for the Cloudini conda package (library + CLI).
2+
#
3+
# Build locally: rattler-build build --recipe conda/recipe.yaml
4+
# Publish: rattler-build upload prefix -c cloudini output/**/*.conda
5+
# (or) rattler-build publish ./conda/recipe.yaml --to https://prefix.dev/cloudini
6+
#
7+
# The same file is used for the conda-forge/staged-recipes submission.
8+
# After cutting the git tag, fill in `sha256` with:
9+
# curl -sL https://github.com/facontidavide/cloudini/archive/refs/tags/${version}.tar.gz | sha256sum
10+
11+
context:
12+
version: "1.2.4"
13+
14+
package:
15+
name: cloudini
16+
version: ${{ version }}
17+
18+
source:
19+
- url: https://github.com/facontidavide/cloudini/archive/refs/tags/${{ version }}.tar.gz
20+
sha256: 0000000000000000000000000000000000000000000000000000000000000000
21+
# mcap is a header-only dependency of the CLI and is not packaged for C++ on
22+
# conda-forge; fetch it here (network is available during the source stage but
23+
# NOT during the build script) and feed it in via -DMCAP_INCLUDE_DIR below.
24+
- url: https://github.com/foxglove/mcap/archive/refs/tags/releases/cpp/v2.0.2.tar.gz
25+
sha256: af44f92b62f43bd4b44a00728780f93df59601537d9aebbc862c4cb9b5ad743e
26+
target_directory: mcap-src
27+
28+
build:
29+
number: 0
30+
script:
31+
# Locate the pre-fetched mcap headers regardless of the archive's top dir.
32+
- MCAP_INCLUDE_DIR="$(find "$PWD/mcap-src" -type d -path '*/cpp/mcap/include' | head -1)"
33+
- cmake ${CMAKE_ARGS} -GNinja -B build -S cloudini_lib
34+
-DCMAKE_BUILD_TYPE=Release
35+
-DBUILD_SHARED_LIBS=ON
36+
-DCLOUDINI_FORCE_VENDORED_DEPS=OFF
37+
-DCLOUDINI_BUILD_BENCHMARKS=OFF
38+
-DBUILD_TESTING=OFF
39+
-DMCAP_INCLUDE_DIR="${MCAP_INCLUDE_DIR}"
40+
-DCMAKE_INSTALL_PREFIX=$PREFIX
41+
- cmake --build build --parallel ${CPU_COUNT}
42+
- cmake --install build
43+
44+
requirements:
45+
build:
46+
- ${{ compiler('c') }}
47+
- ${{ compiler('cxx') }}
48+
- cmake
49+
- ninja
50+
host:
51+
# zstd and lz4-c both declare run_exports, so their runtime dependency is
52+
# added automatically with the correct version pin — no `run:` entries needed.
53+
- zstd
54+
- lz4-c
55+
# cxxopts is header-only (CLI arg parsing); found via find_package(cxxopts).
56+
- cxxopts
57+
58+
tests:
59+
# Assert the package actually shipped the library, headers, CMake config, and CLI.
60+
- package_contents:
61+
include:
62+
- cloudini_lib/cloudini.hpp
63+
lib:
64+
- cloudini_lib
65+
bin:
66+
- cloudini_rosbag_converter
67+
files:
68+
- lib/cmake/cloudini_lib/cloudini_libConfig.cmake
69+
- lib/cmake/cloudini_lib/cloudini_libTargets.cmake
70+
# The CLI binary must load its shared library and start up.
71+
- script:
72+
- cloudini_rosbag_converter --help
73+
74+
about:
75+
homepage: https://github.com/facontidavide/cloudini
76+
repository: https://github.com/facontidavide/cloudini
77+
documentation: https://github.com/facontidavide/cloudini#readme
78+
license: Apache-2.0
79+
license_file: LICENSE
80+
summary: High-performance pointcloud compression library and CLI
81+
description: |
82+
Cloudini is a high-performance pointcloud compression library focused on
83+
speed while still achieving very good compression ratios. It uses a
84+
two-stage approach: custom per-field encoding followed by general-purpose
85+
compression (LZ4/ZSTD). Typical use cases are shrinking rosbag/MCAP datasets
86+
that contain pointclouds and reducing bandwidth when streaming pointclouds
87+
over a network. This package ships the C++ library (with a CMake package
88+
config, `find_package(cloudini_lib)` + `cloudini::cloudini_lib`) and the
89+
`cloudini_rosbag_converter` command-line tool for compressing/decompressing
90+
MCAP rosbags.
91+
92+
extra:
93+
recipe-maintainers:
94+
- facontidavide

0 commit comments

Comments
 (0)