Developer Guide¶
Pixi Workspace¶
This project uses pixi exclusively for package management. Pixi manages all C++ libraries, Python packages, build tools, and platform-specific dependencies through a single lockfile (pixi.lock). No pip, conda, poetry, or venv.
Available Tasks¶
| Task | Description | What it does |
|---|---|---|
build |
Full build | C++ libs + nanobind bindings |
build-cpp |
C++ only | Fetches sources via vcstool, builds 28 packages with colcon |
install |
Install bindings | Editable pip install (depends on build-cpp) |
test |
Run tests | pytest with xdist parallel (depends on install) |
typecheck |
Type check | pyright on src/tesseract_robotics/ |
lint |
Lint | ruff check |
fmt |
Format | ruff format |
docs |
Live docs | mkdocs serve with auto-reload |
docs-build |
Build docs | Static site to site/ |
check-changelog |
Check changelog | CHANGELOG.md release structure (the Docs workflow runs it too) |
Daily Workflow¶
# First time setup (fetches C++ deps, builds everything, ~15-30 min)
pixi run build
# Run tests
pixi run test
# Run a single test file
pixi run python -m pytest tests/trajopt_sqp/test_trajopt_sqp_bindings.py -v
# Run only tests affected by recent changes
pixi run python -m pytest --testmon
# Run an example
pixi run tesseract_freespace_ompl_example
# or
pixi run python -m tesseract_robotics.examples.freespace_ompl_example
# Interactive shell (all env vars set)
pixi shell
python -c "from tesseract_robotics.planning import Robot; print('ok')"
Rebuild After C++ Changes¶
If you modify a C++ binding file (src/*_bindings.cpp):
If you modify upstream C++ sources or dependencies.repos:
Task Dependency Chain¶
Tasks use depends-on for ordered execution:
Running pixi run build executes build-cpp → install automatically. Running pixi run test also triggers the full chain since it depends on install.
Environments¶
The workspace defines multiple Python environments for CI testing:
# Default environment (Python from pixi.lock)
pixi run test
# Specific Python version
pixi run -e py39 test
pixi run -e py312 test
Environments defined in pyproject.toml:
| Environment | Python | Usage |
|---|---|---|
default |
from lockfile | Local development |
py39 |
3.9.x | CI matrix |
py310 |
3.10.x | CI matrix |
py311 |
3.11.x | CI matrix |
py312 |
3.12.x | CI matrix |
Dependency Management¶
All dependencies live in pyproject.toml under [tool.pixi.dependencies] (conda packages) and [tool.pixi.pypi-dependencies] (PyPI packages).
# Add a conda dependency
pixi add some-package
# Add a PyPI dependency
pixi add --pypi some-package
# Update lockfile after editing pyproject.toml manually
pixi install
# See what's installed
pixi list
Critical pins:
taskflow>=3.6,<3.8— 3.8+ requires C++20, we use C++17cereal>=1.3,<2— serialization backend (0.34+)llvm-openmp>=14,<20— macOS OpenMP (not Homebrew's)
CONDA_PREFIX and Worktrees¶
When working in a git worktree, the CONDA_PREFIX env var may point to the main repo's .pixi/envs/default instead of the worktree's. This causes build failures (e.g. missing cereal). Override when building manually:
Using pixi run handles this automatically.
Architecture¶
graph TD
subgraph "C++ Libraries"
A[tesseract_environment]
B[tesseract_kinematics]
C[tesseract_collision]
D[tesseract_planning]
end
subgraph "nanobind Modules"
E[tesseract_environment]
F[tesseract_kinematics]
G[tesseract_collision]
H[tesseract_motion_planners]
end
subgraph "Python API"
I[tesseract_robotics.planning]
J[Robot, MotionProgram, TaskComposer,<br/>plan_freespace / plan_ompl / plan_cartesian]
end
A --> E
B --> F
C --> G
D --> H
E --> I
F --> I
G --> I
H --> I
I --> J
Project Layout¶
tesseract_nanobind/
├── pyproject.toml # pixi workspace + package config
├── pixi.lock # locked deps (~200 packages)
├── CMakeLists.txt # nanobind module build
├── dependencies.repos # C++ source versions (vcstool)
├── src/
│ ├── tesseract_robotics/ # Python package
│ │ ├── planning/ # High-level API (pure Python)
│ │ ├── viewer/ # 3D visualization
│ │ ├── trajopt_ifopt/ # Low-level optimization
│ │ ├── trajopt_sqp/ # SQP solver
│ │ └── <module>/ # nanobind module + __init__.py + .pyi
│ ├── tesseract_nb.h # Shared precompiled header
│ └── <module>/ # C++ binding source (*_bindings.cpp)
├── tests/ # pytest tests
├── examples/ # Usage examples
├── scripts/
│ ├── build_tesseract_cpp.sh # C++ build (colcon + vcstool)
│ ├── generate_stubs.py # Regenerate / check .pyi stubs
│ ├── build_linux_wheel.sh # Portable manylinux wheel (patchelf)
│ └── build_macos_wheel.sh # Portable macOS wheel (delocate)
└── ws/ # C++ workspace
├── src/ # Fetched C++ sources
└── install/ # Built C++ libraries
Cross-Module Type Resolution¶
nanobind maintains separate type registries per module. When a function returns a type from another module, that module must be imported first:
NB_MODULE(_my_module, m) {
// Import module that defines the type BEFORE using it
nb::module_::import_("tesseract_robotics.tesseract_collision._tesseract_collision");
// Now can return DiscreteContactManager from functions
.def("getContactManager", [...] { return self.getDiscreteContactManager(); })
}
See Migration Notes for details.
Type Checking¶
Uses pyright configured via pyrightconfig.json. Key design decisions:
- Python code is type-checked — the
planning/module and other pure Python code - Auto-generated stubs are excluded — nanobind generates
.pyistubs with C++ type artifacts - Hand-written stub exception —
ompl_base/_ompl_base.pyiis manually maintained
When adding new bindings, declare inheritance in nanobind to get correct stubs:
// Good - generates: class Derived(Base)
nb::class_<Derived, Base>(m, "Derived")
// Bad - generates: class Derived (no inheritance)
nb::class_<Derived>(m, "Derived")
Stub Generation¶
After modifying C++ bindings, regenerate the .pyi stubs:
This finds every nanobind extension module in the installed package and writes each
stub to src/tesseract_robotics/<module>/. Each module is rendered by
nanobind.stubgen in a fresh interpreter, because which cross-module types resolve
depends on what is already imported. Stubs are committed for IDE support and type
checking.
Stubs are generated, never hand-edited. Edits made by hand are lost at the next regeneration. Put documentation in the binding's docstring instead.
Drift gate (gh-159). pixi run stubs-check exits 1 when a committed stub differs
from what the build renders. CI runs it in every build job (all platforms, every Python
except 3.9). Stubgen output depends on the nanobind version, so nanobind is pinned
exactly in pyproject.toml. Bumping nanobind means regenerating the stubs in the same
commit.
No std:: in stubs. stubgen writes a C++ type it can't map to Python as a quoted
C++ name. Such a type has no binding or converter, so it also fails at runtime. Spellings
of std:: types differ by platform (libc++ std::__1::, libstdc++ std::__cxx11::),
so such a leak also makes the stub platform-dependent. tests/scripts/test_generate_stubs.py
rejects them. Quoted tesseract::… names are the same kind of gap but
platform-independent. Each one means the type isn't bound, or the module doesn't
nb::module_::import_() the module that binds it.
nanobind's std::unordered_map caster matches default-allocator maps only.
src/tesseract_nb.h adds a caster for Tesseract's Eigen::aligned_allocator maps
(TransformMap and friends).
Pre-commit Hooks¶
| Hook | Stage | What it does |
|---|---|---|
| ruff check --fix | pre-commit | Auto-fix lint issues |
| ruff format | pre-commit | Format code |
| stage-formatted | pre-commit | Auto-stage ruff changes |
| pyright | pre-push | Type check |
| pytest --testmon | pre-push | Run affected tests |
Skip when needed: git commit --no-verify / git push --no-verify
Changelog at Release Time¶
CHANGELOG.md follows Keep a Changelog and is
published on the docs site. scripts/check_changelog.py checks its release
structure; the Docs workflow runs it before building, so a mis-cut release fails
CI instead of publishing a wrong changelog (0.35.0.7 and 0.35.0.8 were both cut
by hand, and both broke it). To cut <version>:
- Rename
## [Unreleased]to## [<version>] - <YYYY-MM-DD>and add a new, empty## [Unreleased]above it. - Add
[<version>]: https://github.com/tesseract-robotics/tesseract_nanobind/compare/<previous>...<version>above the previous release's link. - Point
[Unreleased]:atcompare/<version>...HEAD. - Run
pixi run check-changelogbefore committing.
Anything merged after the tag goes under ## [Unreleased], not into the
release's section.
Contributing¶
- Fork the repository
- Create a feature branch (or use a git worktree)
pixi run build(first time)- Make changes
pixi run testpixi run typecheck- Submit a pull request