Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
ad1a848
Add unified Python wheel configuration
TomCC7 Jun 9, 2026
5b0e8a6
Add cmeel split package manifests
TomCC7 Jun 9, 2026
04616d3
Add root CMake packaging modes
TomCC7 Jun 9, 2026
e4bc400
Prepare native targets for packaged RPATHs
TomCC7 Jun 9, 2026
18e75d6
Install core binding modules for package builds
TomCC7 Jun 9, 2026
80230ed
Install solver binding modules for package builds
TomCC7 Jun 9, 2026
67f0875
Add Python packaging structure tests
TomCC7 Jun 9, 2026
49fa86a
Support VCS source builds with cmeel dependencies
TomCC7 Jun 9, 2026
00be2a0
Fix PR CI packaging build modes
TomCC7 Jun 16, 2026
0ca0117
Prefer Python nanobind discovery in ROS builds
TomCC7 Jun 16, 2026
40fd08a
Add hpp-fcl target compatibility
TomCC7 Jun 16, 2026
cf83c12
Cover hpp-fcl compatibility contract
TomCC7 Jun 16, 2026
c1fb582
Drop Rolling from required ROS CI
TomCC7 Jun 16, 2026
d24724c
Revert "Drop Rolling from required ROS CI"
TomCC7 Jun 16, 2026
74a6093
Rename unified distribution to roboplan-dimos
TomCC7 Jun 20, 2026
1679617
Add PyPI release publishing workflow
TomCC7 Jun 20, 2026
2e449e8
Install wheel libraries under lib for auditwheel
TomCC7 Jun 20, 2026
2919839
Add contained Python packaging helpers
TomCC7 Jun 22, 2026
327a5e2
Route root CMake through packaging helpers
TomCC7 Jun 22, 2026
296be81
Update packaging contract checks
TomCC7 Jun 22, 2026
d5761fc
Add packaging-only CMake entrypoint
TomCC7 Jun 22, 2026
fce21c1
Point cmeel manifests at packaging entrypoint
TomCC7 Jun 22, 2026
3ff51f0
Centralize build tree package configs
TomCC7 Jun 22, 2026
13030d2
Remove root packaging superbuild
TomCC7 Jun 22, 2026
8cb8776
Restore core package CMake files
TomCC7 Jun 22, 2026
a4aa669
Restore downstream native CMake files
TomCC7 Jun 22, 2026
e46a136
Restore binding CMake files
TomCC7 Jun 22, 2026
2bbc24a
Restore package test CMake files
TomCC7 Jun 22, 2026
c72a9f1
Update reduced CMake contract tests
TomCC7 Jun 22, 2026
d31bac9
Ignore packaging helpers during colcon discovery
TomCC7 Jun 22, 2026
a26129d
Update Python packaging helper behavior
TomCC7 Jun 22, 2026
3d882bc
Document Python packaging validation
TomCC7 Jun 22, 2026
eaec8fa
Add stable Python 3.14 wheel support
TomCC7 Jun 22, 2026
6bfbe66
Rename unified distribution back to roboplan
TomCC7 Jun 22, 2026
6f0b17e
Split Python binding build from release publishing
TomCC7 Jun 22, 2026
ff91d52
Rename PyPI wheel build workflow
TomCC7 Jun 23, 2026
e88d08e
Simplify cmeel Python wheel controls
TomCC7 Jun 23, 2026
da34dbb
Document PyPI wheel build scope
TomCC7 Jun 23, 2026
f5160e9
Add arm and macOS PyPI wheel builds
TomCC7 Jun 23, 2026
7c9c95b
Set C++ standard for wheel builds
TomCC7 Jun 23, 2026
e4c96f0
Repair macOS wheel rpaths
TomCC7 Jun 23, 2026
36a6994
Run PyPI wheel builds on main pushes
TomCC7 Jun 23, 2026
1f73a06
Set macOS wheel deployment targets
TomCC7 Jun 23, 2026
f90938f
Simplify standalone binding aliases
TomCC7 Jun 23, 2026
02bbb24
Remove split cmeel packaging path
TomCC7 Jun 23, 2026
e4a2d37
Publish releases only to PyPI
TomCC7 Jun 23, 2026
aac3ad0
Remove stale packaging option assertions
TomCC7 Jun 23, 2026
3585621
Clean up unified packaging tests
TomCC7 Jun 23, 2026
65feae4
Apply suggestions from code review
TomCC7 Jun 23, 2026
690f3a4
Derive wheel version from git tags
TomCC7 Jun 24, 2026
287fbd6
Use project version for Python bindings
TomCC7 Jun 24, 2026
2774363
Fixes from testing
sea-bass Jun 25, 2026
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
142 changes: 142 additions & 0 deletions .github/workflows/build-pypi-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Build PyPI wheels

on:
pull_request:
push:
branches:
- main
workflow_dispatch:
workflow_call:

permissions:
contents: read

jobs:
check_version:
name: Check package version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.package.outputs.version }}
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- name: Read package metadata
id: package
run: |
python -m pip install --upgrade "setuptools-scm>=8"
python - <<'PY' >> "$GITHUB_OUTPUT"
import tomllib
from pathlib import Path

from setuptools_scm import get_version

pyproject = tomllib.loads(Path("pyproject.toml").read_text())
project = pyproject["project"]
if project["name"] != "roboplan":
raise SystemExit(f"Unexpected package name: {project['name']}")
if "version" in project:
raise SystemExit("pyproject.toml must use git-derived dynamic versioning")
if project.get("dynamic") != ["version"]:
raise SystemExit("pyproject.toml must declare dynamic = ['version']")
print(f"version={get_version(root='.', local_scheme='no-local-version')}")
PY
- name: Check tag matches package version
if: startsWith(github.ref, 'refs/tags/')
env:
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
if ! [[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag $RELEASE_TAG must use N.N.N format"
exit 1
fi
if [ "$RELEASE_TAG" != "$PACKAGE_VERSION" ]; then
echo "::error::Tag $RELEASE_TAG does not match package version $PACKAGE_VERSION"
exit 1
fi

build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
needs: check_version
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build sdist
run: |
rm -rf dist
python -m pip install --upgrade build twine
python -m build --sdist
python -m twine check dist/*.tar.gz
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: python-distributions-sdist
path: dist/roboplan-*.tar.gz
if-no-files-found: error

build_wheels:
name: Build ${{ matrix.name }} wheels
runs-on: ${{ matrix.os }}
needs: check_version
strategy:
fail-fast: false
matrix:
include:
- name: Linux x86_64
os: ubuntu-latest
archs: x86_64
artifact: linux-x86_64
extra_environment: ""
- name: Linux aarch64
os: ubuntu-24.04-arm
archs: aarch64
artifact: linux-aarch64
extra_environment: ""
- name: macOS arm64
os: macos-14
archs: arm64
artifact: macos-arm64
extra_environment: "MACOSX_DEPLOYMENT_TARGET=14.0"
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- name: Build wheels
uses: pypa/cibuildwheel@v4.1.0
env:
CIBW_ARCHS: ${{ matrix.archs }}
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*"
Comment thread
TomCC7 marked this conversation as resolved.
CIBW_SKIP: "pp* *-musllinux*"
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
CIBW_ENVIRONMENT: >-
CMAKE_BUILD_PARALLEL_LEVEL=2
MAKEFLAGS="-j2"
NINJAFLAGS="-j2"
Comment thread
TomCC7 marked this conversation as resolved.
${{ matrix.extra_environment }}
CIBW_TEST_COMMAND: >-
python -c "import roboplan; import roboplan.core; import roboplan.filters; import roboplan.example_models;
import roboplan.simple_ik; import roboplan.optimal_ik; import roboplan.rrt;
import roboplan.toppra; import roboplan.cartesian_planning; print('roboplan imports ok')"
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: python-distributions-${{ matrix.artifact }}
path: wheelhouse/roboplan-*.whl
if-no-files-found: error
43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release

on:
push:
tags:
- "[0-9]*.[0-9]*.[0-9]*"
workflow_dispatch:

permissions:
contents: read

jobs:
build_pypi_wheels:
name: Build PyPI wheels
uses: ./.github/workflows/build-pypi-wheels.yml

publish_pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: build_pypi_wheels
if: startsWith(github.ref, 'refs/tags/')
environment:
name: pypi
url: https://pypi.org/project/roboplan/
permissions:
contents: read
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v5
with:
pattern: python-distributions-*
merge-multiple: true
path: dist
- name: Check distributions
run: |
python -m pip install --upgrade twine
ls -la dist
python -m twine check dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
.pixi
*.egg-info

# Python packaging artifacts
dist/
wheelhouse/

# Benchmarking results
pytest_benchmarks.json
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The main folders found in this repo are as follows.
- `roboplan_cartesian_planning` : A Cartesian planner for following task-space paths.
- `roboplan_example_models` : Contains robot models used for testing and examples.
- `roboplan_examples` : Basic examples with real robot models.
- `packaging` : Files to help package wheels for release on PyPi.

---

Expand Down
Empty file added packaging/COLCON_IGNORE
Empty file.
29 changes: 29 additions & 0 deletions packaging/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.22)

set(ROBOPLAN_PACKAGING_VERSION "0.0.0")
if(DEFINED SKBUILD_PROJECT_VERSION)
set(ROBOPLAN_PACKAGING_VERSION "${SKBUILD_PROJECT_VERSION}")
endif()
project(roboplan VERSION "${ROBOPLAN_PACKAGING_VERSION}")

get_filename_component(ROBOPLAN_REPOSITORY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)

include(cmake/roboplan_python_packaging.cmake)

if(SKBUILD)
roboplan_configure_scikit_build_prefix()
endif()

roboplan_register_build_tree_packages()

add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_example_models" "roboplan_example_models")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan" "roboplan")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_simple_ik" "roboplan_simple_ik")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_oink" "roboplan_oink")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_rrt" "roboplan_rrt")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_toppra" "roboplan_toppra")
add_subdirectory("${ROBOPLAN_REPOSITORY_ROOT}/roboplan_cartesian_planning" "roboplan_cartesian_planning")

if(SKBUILD)
roboplan_configure_unified_python_wheel()
endif()
68 changes: 68 additions & 0 deletions packaging/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Python packaging
Comment thread
TomCC7 marked this conversation as resolved.

This directory contains the PyPI/cibuildwheel-specific packaging helpers for RoboPlan.
The normal source-tree CMake build remains in the package directories.
`pyproject.toml` points `scikit-build-core` at this directory with `tool.scikit-build.cmake.source-dir`.

The helper CMake files cover three packaging-only concerns:

- expose `cmeel`'s isolated native dependency prefix to `scikit-build-core` builds;
- install the runtime native libraries that wheels need at import time;
- repair installed RPATHs with `patchelf` using checked-in CMake script templates rather than large embedded `install(CODE ...)` strings.

Release wheels are built with `cibuildwheel` in the [`build-pypi-wheels.yml`](../../.github/workflows/build-pypi-wheels.yml) CI workflow, and are smoke-tested by importing the `roboplan` namespace and every compiled submodule.
That workflow runs on pull requests so wheel build breakage is caught before release.
The [`release.yml`](../../.github/workflows/release.yml) workflow invokes the same build workflow from the tagged commit, downloads those fresh artifacts, and publishes them through PyPI trusted publishing.


## Local checks

Run commands from the repository root. Keep native parallelism capped on small machines, matching CI:

```bash
export CMAKE_BUILD_PARALLEL_LEVEL=2
export MAKEFLAGS=-j2
export NINJAFLAGS=-j2
```

Source-build and import-test the unified wheel path:

```bash
uv venv --seed /tmp/roboplan-wheel-check
uv pip install -v --python /tmp/roboplan-wheel-check/bin/python --no-cache .
/tmp/roboplan-wheel-check/bin/python - <<'PY'
import roboplan
import roboplan.core
import roboplan.filters
import roboplan.example_models
import roboplan.simple_ik
import roboplan.optimal_ik
import roboplan.rrt
import roboplan.toppra
import roboplan.cartesian_planning
print("roboplan imports ok")
PY
```

Check the structural packaging contract:

```bash
uvx pytest tests/unified_python_package_test.py -q
```

## Run an example against the wheel

To prove the wheel is usable end-to-end, install it into a fresh environment and run one of the examples.
The examples are not part of the wheel, so they pull in a few extra runtime dependencies on top of `roboplan` itself.

```bash
# 1. Create an isolated environment and install the wheel we just built.
uv venv --seed /tmp/roboplan-wheel-check
uv pip install --python /tmp/roboplan-wheel-check/bin/python --no-cache .

# 2. Install the extra dependencies the examples need.
uv pip install --python /tmp/roboplan-wheel-check/bin/python pycollada tyro xacro viser

# 3. Run an example.
/tmp/roboplan-wheel-check/bin/python roboplan_examples/python/example_ik.py
```
55 changes: 55 additions & 0 deletions packaging/python/cmake/repair_unified_macos_rpaths.cmake.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function(_roboplan_resolve_macos_patch_file file result)
set(_patch_file "${file}")
if(IS_SYMLINK "${_patch_file}")
file(READ_SYMLINK "${_patch_file}" _symlink_target)
if(IS_ABSOLUTE "${_symlink_target}")
set(${result} "" PARENT_SCOPE)
return()
endif()
get_filename_component(_symlink_dir "${_patch_file}" DIRECTORY)
set(_patch_file "${_symlink_dir}/${_symlink_target}")
endif()

set(${result} "${_patch_file}" PARENT_SCOPE)
endfunction()

function(_roboplan_add_macos_rpath file rpath)
_roboplan_resolve_macos_patch_file("${file}" _patch_file)
if(NOT _patch_file OR NOT EXISTS "${_patch_file}")
return()
endif()

execute_process(
COMMAND otool -l "${_patch_file}"
OUTPUT_VARIABLE _otool_output
ERROR_QUIET
RESULT_VARIABLE _otool_result
)
if(NOT _otool_result EQUAL 0)
return()
endif()

string(FIND "${_otool_output}" "path ${rpath} (offset" _existing_rpath)
if(_existing_rpath GREATER_EQUAL 0)
return()
endif()

execute_process(
COMMAND chmod u+w "${_patch_file}"
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND "@ROBOPLAN_UNIFIED_INSTALL_NAME_TOOL@" -add_rpath "${rpath}" "${_patch_file}"
COMMAND_ERROR_IS_FATAL ANY
)
endfunction()

file(GLOB_RECURSE _roboplan_extensions "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/roboplan/*.so")
foreach(_file IN LISTS _roboplan_extensions)
_roboplan_add_macos_rpath("${_file}" "@loader_path/../../lib")
endforeach()

file(GLOB _roboplan_libraries "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/lib/*.dylib")
foreach(_file IN LISTS _roboplan_libraries)
_roboplan_add_macos_rpath("${_file}" "@loader_path")
endforeach()
35 changes: 35 additions & 0 deletions packaging/python/cmake/repair_unified_rpaths.cmake.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
function(_roboplan_patch_rpath file rpath)
set(_patch_file "${file}")
if(IS_SYMLINK "${_patch_file}")
file(READ_SYMLINK "${_patch_file}" _symlink_target)
if(IS_ABSOLUTE "${_symlink_target}")
return()
endif()
get_filename_component(_symlink_dir "${_patch_file}" DIRECTORY)
set(_patch_file "${_symlink_dir}/${_symlink_target}")
endif()

if(EXISTS "${_patch_file}")
execute_process(
COMMAND chmod u+w "${_patch_file}"
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND "@ROBOPLAN_UNIFIED_PATCHELF@" --set-rpath "${rpath}" "${_patch_file}"
COMMAND_ERROR_IS_FATAL ANY
)
endif()
endfunction()

file(GLOB_RECURSE _roboplan_extensions "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/roboplan/*.so")
foreach(_file IN LISTS _roboplan_extensions)
_roboplan_patch_rpath("${_file}" "$ORIGIN/../../lib:$ORIGIN/../../cmeel.prefix/lib")
endforeach()

file(GLOB _roboplan_libraries
"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/lib/libfilters.so*"
"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/lib/libroboplan*.so*"
)
foreach(_file IN LISTS _roboplan_libraries)
_roboplan_patch_rpath("${_file}" "$ORIGIN:$ORIGIN/../cmeel.prefix/lib")
endforeach()
Loading
Loading