Toolchain cheatsheet
Always use just and uv. Never pip install inside the workspace — use uv add <pkg> --package <member>. Never call colcon build outside just unless you know why.
Bootstrap (Ubuntu 22/24, macOS 14+)
just bootstrap # installs uv, ROS 2, system deps via scripts/
Python workspace
just sync # resolve & install all workspace deps
# (wraps `uv sync` + scripts/repair_hf_libero_install.py)
just sync --group robocasa # + an opt-in dep group (sim / libero / robocasa / rldx / …)
# → read "Managing the Python environment & dependency
# groups" below before swapping groups or running RoboCasa
just test # run unit tests (<30 s)
just test-doctest # run docstring examples on the curated set
uv run pytest -k so100 # filter by keyword
just lint # ruff check + ruff format --check + mypy --strict
# (mypy via tools/run_mypy.sh: every git-tracked .py — the
# python/* packages and their tests, every colcon package,
# launch files, tests/, tools/, rskills/, scripts; a guard
# fails the run if a new file lands outside every lane)
uv run ruff check . --fix # autofix
uv run ruff format . # format
Managing the Python environment & dependency groups
Read before touching the venv. The rules, in priority order:
1. Always just sync, never bare uv sync. The Justfile sync recipe
wraps scripts/repair_hf_libero_install.py
before and after the sync and forces --all-packages for you. Bare
uv sync skips the repair and trips on:
error: Unable to uninstall hf-libero==0.1.3. distutils-installed distributions
do not include the metadata required to uninstall safely.
which aborts the resolve and leaves the venv half-broken (missing h5py,
transformers, scipy, …). just sync is idempotent and safe on every host.
On aarch64 Linux, just sync needs download.pytorch.org reachable. PyPI's
aarch64 torch wheel is CPU-only, so the root pyproject.toml pins torch +
torchvision to the cu130 index (Jetson Thor is sm_110; see
aarch64-support.md) under a
platform_machine == 'aarch64' marker; without it every in-process policy runs
on CPU on a DGX Spark / Jetson. x86_64 resolution is untouched and never
contacts that host.
Upgrading an existing aarch64 venv also needs a one-time
just sync --frozen --reinstall-package torchvision, because both torchvision
builds share the version string. Details:
aarch64 CUDA hosts.
2. Opt-in dependency groups → just sync --group <name>. The
[dependency-groups] in pyproject.toml define sim, libero, robocasa,
metaworld, maniskill3, rldx, … Heavy runtime deps (transformers>=5.4.0,<5.6.0,
scipy, opencv, robosuite) live in these groups, not in the core deps.
A default sync (no --group) deliberately removes them, so VLA / sim work
needs at least the sim group:
just sync --group sim # minimum working VLA / sim baseline
just sync --group libero # LIBERO suites
just sync --group robocasa # RoboCasa robosuite + supporting deps
just sync --group metaworld # MetaWorld
just sync --group maniskill3 # ManiSkill3 / SAPIEN
just sync --group robocasa is exactly uv sync --all-packages --group robocasa:
the wrapper supplies --all-packages so the editable openral-* members are
never silently uninstalled (see the openral_core entry under
Tooling self-help).
3. The libero ↔ robocasa conflict — swap groups per task. LIBERO pins
robosuite==1.4 + a specific MuJoCo; RoboCasa needs robosuite>=1.5 + a
different MuJoCo. They are declared mutually exclusive in pyproject.toml
([tool.uv] conflicts) and cannot coexist in one resolution:
uv sync --group libero --group robocasa fails by design. Swap the active
group per task:
just sync --group robocasa # before a RoboCasa run
just sync --group sim # (or --group libero) to go back to LIBERO/MuJoCo
4. RoboCasa itself is installed editable AT RUNTIME — not by just sync.
The robocasa group only provides robosuite + supporting deps. The RoboCasa
kitchen fork (a GitHub repo, no PyPI release) is git-cloned and
uv pip install -e --no-deps'd by
openral_sim._deps.ensure_backend_deps('robocasa_kitchen'), which the
deploy-sim HAL triggers from on_configure. Auto-install is on by default
(OPENRAL_AUTO_INSTALL_DEPS unset or =1; =0 prompts instead). Let the HAL
provision it:
OPENRAL_AUTO_INSTALL_DEPS=1 openral deploy sim \
--config scenes/deploy/robocasa_navigate.yaml
deploy sim takes no --rskill: the reasoner picks the active rSkill
dynamically from the in-tree rskills/ palette at on_configure. The scene
YAML is the only policy input.
Do not hand-install robocasa / robosuite: that pulls the wrong robosuite
and wrecks the managed env.
5. Pre-build to skip the in-on_configure install window, so the lifecycle
transition doesn't stall on a first-run build:
OPENRAL_AUTO_INSTALL_DEPS=1 just sync --group robocasa
OPENRAL_AUTO_INSTALL_DEPS=1 python -c \
"from openral_sim._deps import ensure_backend_deps; ensure_backend_deps('robocasa_kitchen')"
6. Never uv sync --all-packages to "repair" the RoboCasa env. Once the
runtime-editable RoboCasa install is in place, a plain uv sync --all-packages
uninstalls it (and the matching robosuite). Repair with
just sync --group robocasa and, if RoboCasa itself was evicted, re-run the
ensure_backend_deps('robocasa_kitchen') line from point 5.
ROS 2
just ros2-build # colcon build of the packages the recipe lists (msgs,
# hal_node, world_state, reasoner_ros, safety_kernel,
# rskill_ros, perception_ros, …)
just ros2-test # colcon test + colcon test-result --verbose
source install/setup.bash # after build
Adding a Python node? Commit it mode 100755. just ros2-build passes
--symlink-install, so install(PROGRAMS …) symlinks the node into
install/lib/<pkg>/; CMake's "make it executable on install" never happens and
the entry inherits the source file's mode. launch_ros resolves executable=
with shutil.which() over that directory, gets None for a 0644 file, and
raises executable '<name>' not found on the libexec directory, abandoning the
entire launch description, not just the one node.
chmod +x alone leaves a 100644 blob in the commit; use
git update-index --chmod=+x <file>. tests/unit/test_ros_node_exec_bits.py
enforces this for every package.
GPU motion planning — cuMotion (optional)
NVIDIA Isaac ROS cuMotion is a CUDA-accelerated MoveIt planning pipeline
(backed by cuRobo). The rskill-moveit-* family uses it automatically when the
host clears the GPU floor in RobotCapabilities.supports_cumotion() (Ampere+,
compute capability ≥ 8.0, CUDA ≥ 13, ~8 GB VRAM), by setting
MotionPlanRequest.pipeline_id; otherwise MoveIt keeps OMPL. It is a
per-request pipeline choice: no new rSkill, no move_group re-launch.
just bootstrap # auto-installs cuMotion when a capable NVIDIA GPU
# is present on jazzy (skipped on CPU hosts → OMPL)
Manual install (if you skipped bootstrap or added the GPU later):
# 1. Add NVIDIA's Isaac ROS apt repo (one-time):
# https://nvidia-isaac-ros.github.io/getting_started/isaac_apt_repository.html
# 2. Install the cuMotion MoveIt pipeline (self-contained C++/CUDA, no cuRobo pip):
sudo apt install ros-$ROS_DISTRO-isaac-ros-cumotion-moveit \
ros-$ROS_DISTRO-isaac-ros-cumotion-robot-description
# 3. Add the cuMotion pipeline to your moveit_config's planning_pipelines, then
# generate the per-robot cuRobo collision config from the kernel's own geometry
# (NVIDIA ships configs for franka / ur5e / ur10e; --emit-cumotion covers the
# rest of the OpenRAL fleet):
openral collision lower --robot robots/<robot>/robot.yaml \
--emit-cumotion robots/<robot>/cumotion_spheres.yaml --write
An MJCF-lowered robot whose collision geometry is meshes (the OpenArm) is
fitted from its MJCF the same way a URDF robot is from its URDF: one fitter
(fit_link_primitives) picks one capsule, one box or a capsule chain per link
from the link's collision and visual geometry, by least mean protrusion
(docs/reference/collision-geometry-review.md). --tight-link <link>
(repeatable) lowers a link as a box plus its exact convex hull
(tight_geometry) instead; a link the manifest already refines stays refined.
The loosening guard still applies: delete the old entries first when the fit
is larger.
Isaac ROS 4.4+ cuMotion is a self-contained C++/CUDA apt package (native
libcumotion.so.1, CUDA 13 runtime): no Python cuRobo and no uv/pip
group. The apt packages are the supported path on OpenRAL's Python 3.12 + Jazzy
stack. cuMotion never bypasses the safety
kernel: planned trajectories still replay through /openral/candidate_action
and are validated waypoint-by-waypoint.
Sim
just sim-eval <config> # canonical config-driven entry point
# (`openral sim run --config FILE`)
just sim-libero # SmolVLA × LIBERO (real lerobot[libero]; needs GPU + MUJOCO_GL)
just sim-xvla-libero # xVLA × LIBERO (Florence-2 backbone)
just sim-pi05-libero # π0.5 × LIBERO (≥8 GB VRAM)
just sim-metaworld --task TASK # SmolVLA × MetaWorld (e.g. --task metaworld/reach-v3)
just sim-act-aloha # ACT × gym-aloha bimanual cube transfer
just sim-diffusion-pusht # Diffusion Policy × gym-pusht (CPU)
just sim-custom # custom example — ACT × gym-aloha insertion
Hardware-in-loop (requires connected robot + USB perms)
just hil franka_panda # runs tests/hil/test_<name>.py (also ur5e, ur10e, sawyer, aloha,
# galaxea_a1, openarm_can_live, …; no SO-100 / G1 HIL file)
just hil-so101 # SO-101 non-motion serial gate
just hil-openarm-deploy # OpenArm full-graph gate (attaches to a running graph)
Docs
just docs # mkdocs serve at :8000
just docs-build # mkdocs build --strict (CI parity)
just schema-export # regenerate JSON Schema (CI compares)
CLI (openral)
just quickstart installs a ~/.local/bin/openral wrapper, so openral runs from any terminal without just. To (re-)install it alone, e.g. after moving the repo:
just install-cli
The wrapper sources the ROS 2 distro overlay and the colcon workspace overlay, then delegates to .venv/bin/openral, so ROS 2 node/topic/action commands just work. Pure-Python commands (openral doctor, openral detect, …) work before ROS 2 is built.
Which checkout does openral run? (OPENRAL_REPO_ROOT)
The wrapper bakes in the checkout that generated it. Right for a single clone, but a provenance hazard once you have two (say, a validation worktree): it execs the generating checkout's venv, colcon overlay and robots/ manifests wherever you invoke it, so a run can be attributed to the wrong branch with nothing in the log to show it. OPENRAL_REPO_ROOT overrides it:
| Situation | What runs | What you see |
|---|---|---|
OPENRAL_REPO_ROOT unset, cwd anywhere in one checkout |
the baked-in checkout | nothing (unchanged) |
OPENRAL_REPO_ROOT=/path/to/checkout |
that checkout | openral: repo root /path/to/checkout (OPENRAL_REPO_ROOT override; installed default …) on stderr |
OPENRAL_REPO_ROOT points at a tree with no executable .venv/bin/openral |
nothing — exit 1 | an error naming the tree; it never falls back to the baked one |
OPENRAL_REPO_ROOT unset, but cwd is inside a different checkout that has its own .venv/bin/openral |
still the baked-in checkout | WARNING: cwd is inside the OpenRAL checkout …, but this launcher is baked to … on stderr |
So when you validate from a worktree, either export the override:
export OPENRAL_REPO_ROOT=$(git rev-parse --show-toplevel)
openral deploy sim --config … # stderr records the root that ran
…or re-run just install-cli from that worktree to re-bake the default. The stderr line is deliberate: a captured run log always names the tree that produced it.
Bare openral (no args) drops into an interactive REPL where subcommands run without the prefix (sim run --config …); pass a subcommand for one-shot mode in scripts/CI.
openral doctor # diagnose host: Python, OS, ROS 2 distro, GPU, USB
openral detect # auto-detect robot + sensors + GPU; write a full robot.yaml
openral connect --robot so100 # open a HAL connection (so100 and so101 — shared SO100FollowerHAL)
openral calibrate camera --sensor S # ros2 camera_calibration helper
openral install sim # post-install opt-in dep groups
openral install ros # run the packaged bootstrap_ubuntu.sh (sudo); no clone needed
openral install list # show every known dep group
openral rskill install <hub-id> # download an rSkill from HF Hub (license-gated)
openral rskill list # list installed rSkills
openral rskill new <id> # scaffold a new local rSkill from rskills/template/
openral sensor list # browse the sensor catalog
openral sensor show <id> # resolve a catalog entry to a SensorSpec/Bundle
openral benchmark run --suite S --rskill R # run a benchmark suite (canonical eval producer)
openral benchmark report # aggregate rskills/<id>/eval/*.json benchmark blocks
openral sim run --config FILE # run a SimScene YAML end-to-end
openral prompt "pick the cube" # one-shot operator prompt to the prompt-router (ROS)
openral estop reset # clear every e-stop latch: kernel reset, then /openral/estop_cleared (ROS)
Tooling self-help
openralprintsAMENT_TRACE_SETUP_FILES: unbound variableand exits? Your~/.local/bin/openralpredates the fix that sources the (not-set -u-safe) ROS 2 overlays with nounset disabled. Re-runjust install-clito regenerate it.openralseems to run the wrong branch / wrongrobots/manifests? You are in a second checkout while~/.local/bin/openralis baked to the first (seeOPENRAL_REPO_ROOT). NoWARNING: cwd is inside the OpenRAL checkout …line means your wrapper predates that check; regenerate it withjust install-cli.- Python import unclear?
uv run python -c 'import openral_<pkg>; print(openral_<pkg>.__file__)'. - ROS 2 topic missing?
ros2 topic list -tin asource install/setup.bash-ed shell. - Schema diff?
just schema-exportand checkgit diff docs/reference/schemas/. - CI flake? Re-run once. If it flakes again, triage with the
flakelabel and don't merge. - Hardware test fails on a runner? Check the runner's e-stop log first. Never push a "fix" that makes a hardware test pass without understanding why it failed.
- Out of GPU memory? Lower batch / quantize / use a smaller skill variant — never silently downcast or skip frames.
uv syncfails with "Unable to uninstallhf-libero==0.1.3. distutils-installed distributions..."? The PyPI sdist forhf-liberodrops a spurious top-level*.egg-infoFILE next to the proper.dist-info/directory, and uv misclassifies the install as distutils-built. Runjust sync <flags>: it pre/post-runsscripts/repair_hf_libero_install.pyto strip the bogus file + RECORD line.ModuleNotFoundError: No module named 'cv2'after switching groups?opencv-python(pulled by some groups) andopencv-python-headless(anopenral-clidependency) both install intocv2/; uninstalling the former deletes the shared directory while the latter still reads as installed.just syncdetects the missingcv2and reinstallsopencv-python-headlessat its locked version.No module named 'openral_core'after switching dependency groups? You ran bareuv sync --group <x>. Without--all-packagesthat syncs only the workspace root and uninstalls every editableopenral-*member, so the next REPL /openral deploy simcan't import the workspace.just syncforces--all-packages(unless you scoped to a single--package/-p). Repair an already-broken venv withjust sync --all-packages.