diff --git a/README.md b/README.md index 8c55675..db7ed52 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,58 @@ # Lucy ROS 2 workspace (Jazzy) -Workspace bringup for the Lucy / InMoov humanoid. Everything (ROS 2 Jazzy, Gazebo, RViz, the web control panel) runs inside a single Docker container — you only need **Docker**, **Git** and **Python 3** on the host (plus **`xhost`** on Linux for GUI forwarding; on macOS the GUI is viewed over VNC with no extra software — see [GUI: RViz and Gazebo](#gui-rviz-and-gazebo)). +Workspace bringup for the Lucy / InMoov humanoid. ROS 2 Jazzy, Gazebo, RViz, and the web control panel run **natively** via [Pixi](https://pixi.prefix.dev/) and [RoboStack](https://robostack.github.io/) — no Docker required for day-to-day development. ## Requirements -- [Python3](https://www.python.org/downloads/) -- [Docker](https://docs.docker.com/engine/install/) -- [Git](https://git-scm.com/downloads) +| Requirement | Linux | macOS | Windows | +|-------------|-------|-------|---------| +| [Git](https://git-scm.com/downloads) | ✓ | ✓ | ✓ ([Git for Windows](https://git-scm.com/install/windows)) | +| [Python 3](https://www.python.org/downloads/) (for `Lucy.py`) | ✓ | ✓ | ✓ | +| [Pixi](https://pixi.prefix.dev/latest/installation/) **≥ 0.78** | ✓ | ✓ | ✓ | +| **tmux** (multi-window launcher) | ✓ | ✓ (`brew install tmux`) | — (launcher runs directly) | -Linux GUI forwarding uses `xhost` (preinstalled). On Wayland run `xhost +local:docker` if windows don't open — see [GUI](#gui-rviz-and-gazebo). +ROS packages are installed by Pixi into `.pixi/`; you do not need a system ROS install. -> **Windows users:** see the [Windows README](windows/README.md) — **`Lucy-Setup.exe`** to install/update, **`Lucy.exe`** to launch. +GUI apps (RViz, Gazebo, rqt) use your **native display**. Platform-specific notes (Wayland, AirPlay port conflicts) are in the [developer guide](docs/developer_lucy_packages.md#platform-setup). ## Get the repository -Install the [requirements](#requirements) first, then grab the repo. You can clone it with Git (recommended — makes updates a `git pull`) or download a ZIP. - -**Option A — Clone with Git (recommended):** +**Clone (recommended):** ```bash git clone https://github.com/Sentience-Robotics/lucy_ws.git +cd lucy_ws ``` -**Option B — Download the ZIP:** +**Or** download the ZIP from GitHub (**Code → Download ZIP**), extract, and `cd lucy_ws`. -- Open the repository page on GitHub, click the green **Code** button, then **Download ZIP**, and extract it. +Run `Lucy.py` and the install scripts **from the repository root** — they read `config/` and paths relative to that directory. -Then open a terminal and move into the project folder before running anything: +## Install + +### Linux / macOS ```bash -cd lucy_ws +curl -fsSL https://pixi.sh/install.sh | bash # if Pixi is not installed +export PATH="$HOME/.pixi/bin:$PATH" # if needed +./install.sh ``` -> The manager (`Lucy.py`) must be run **from the repository root** — it reads `config/`, `docker/Dockerfile.jazzy` and mounts the workspace relative to that directory. +### Windows + +**End users:** download **`Lucy-Setup.exe`** from [GitHub Releases](https://github.com/Sentience-Robotics/lucy_ws/releases). It installs Lucy, clones sub-repos, runs `pixi install`, and builds the workspace. See the [Windows README](windows/README.md). + +**Developers (from source):** + +```powershell +python windows\Lucy.py --cli install --repos-branch master +``` + +Or from Git Bash: `./install.sh` ## Quick start -A Python-based Text User Interface (TUI) manages the whole workspace — installing, rebuilding, and launching the environment. From the repository root, run the manager for your platform: +After install, launch the **Lucy manager** TUI from the repository root. ### Linux / macOS @@ -44,53 +60,50 @@ A Python-based Text User Interface (TUI) manages the whole workspace — install python3 Lucy.py ``` -> On UNIX systems, if you ever run the scripts by hand, `chmod +x install.sh launch_lucy.sh` first. `./install.sh` is only a fallback to `Lucy.py`. - ### Windows -**Installer (recommended):** download `Lucy-Setup.exe` from [GitHub Releases](https://github.com/Sentience-Robotics/lucy_ws/releases), then see the [Windows README](windows/README.md). +**End users:** open **Lucy** from the Start Menu (runs `Lucy.exe` → Control Center). -**From source (developers):** +**Developers:** -```bash -python windows/Lucy.py --cli install # first time -python windows/Lucy.py # launch +```powershell +python windows\Lucy.py ``` -> **Windows** additionally needs a third-party X Server for RViz/Gazebo — see the [Windows README](windows/README.md). End users should use **`Lucy-Setup.exe`** instead of manual install. +Or Git Bash: `./launch_lucy.sh` -### Opening the Control Panel +## Using the Lucy launcher After **`Launch`**, enable **Core + Control Panel** in the launcher. Once it is running, the **Lucy Control Panel is accessible in your browser at [http://localhost:4004](http://localhost:4004)** (or the next free port if 4004 is already taken). The launcher also shows the exact URL next to the Control Panel entry once it's up. +| Key | Action | +|-----|--------| +| **Up/Down** | Navigate | +| **Space** | Toggle a package or tool | +| **Enter** | Apply changes / Start / Restart | +| **X** | Stop all processes and exit | -## Using the workspace +**Components you can enable:** -Selecting **Launch** from the manager starts everything inside a single **tmux** session in the Docker container. You first land on the **Lucy Control Center** TUI: +- **Core** — base robot stack (`lucy_bringup`) +- **Modifiers** — Simulator (Gazebo), **… headless** (server-only sim, under Simulator), Visualizer (RViz), Real Hardware +- **Interfaces** — Control Panel (web UI), Lucy CLI +- **Tools** — Console, rqt -- **Up/Down arrows** — navigate -- **Space** — toggle a package or tool on/off -- **Enter** — apply your changes (new tools open in their own background windows) -- **X** — stop all processes and exit the container +> **Recommended starting point:** **Core + Control Panel** — the web 3D viewer is enough for most work without heavy GUI apps. -### Launch options +**Control panel:** after Launch, enable **Core + Control Panel**. Open [http://localhost:5000](http://localhost:5000) (or the URL shown in the launcher if another port was chosen). -What you toggle in the launcher falls into a few groups: +**tmux windows** (Linux/macOS): -- **Core** — the base robot software stack (*lucy_bringup*), everything else builds on it. -- **Modifiers** (extend Core): - - **Simulator (Gazebo)** — physics simulation - - **Visualizer (RViz)** — ROS 3D visualizer - - **Real Hardware** — connect to the physical robot -- **Interfaces:** - - **Control Panel** — web UI with a built-in 3D viewer - - **Lucy CLI** — terminal control interface -- **Tools** — Console, rqt, and VNC viewers. +- **`Ctrl+B` then `W`** — window list +- **`Ctrl+B` then `N`** / **`P`** — next / previous window -Gazebo, RViz and rqt are native GUI apps and need a display — native X11 or the VNC desktop (see [GUI: RViz and Gazebo](#gui-rviz-and-gazebo)). Each interface/viewer that exposes a URL shows it right in the launcher once it's running. +On Windows, the Control Center runs without tmux (one process tree). Gazebo, RViz, and rqt still open as native GUI apps when enabled. -> **Recommended starting point:** enable **Core + Control Panel**. The control panel's web 3D viewer is usually enough to get going and avoids the heavier GUI apps (Gazebo/RViz) entirely. +## Developer setup +<<<<<<< HEAD ### Managing tmux windows Tools (the console, CLI, viewers…) run in background windows, so a few `tmux` basics help you move between them: @@ -159,3 +172,6 @@ the address it shows. Defaults: - [`docs/developer_lucy_packages.md`](docs/developer_lucy_packages.md) — developer guide: per-repo docs, all `install.sh` / `launch_lucy.sh` flags, dev mode, ports, environment overrides, packages overview. - [`docs/launcher_packages.md`](docs/launcher_packages.md) — launcher guide: how to add new packages to the launcher UI and understand the configuration fields. +======= +For developer mode, Pixi component tasks (`pixi run core`, `sim-headless`, …), debug shell, SSH clones, local repo overrides, ports, and advanced launch options, see the **[developer guide](docs/developer_lucy_packages.md)**. +>>>>>>> 41e84e8 (evol(windows,docs): update Windows support and documentation) diff --git a/docs/developer_lucy_packages.md b/docs/developer_lucy_packages.md index d8db69b..4ac92d7 100644 --- a/docs/developer_lucy_packages.md +++ b/docs/developer_lucy_packages.md @@ -1,54 +1,29 @@ -# Lucy `lucy_ws` developer guide +# Lucy developer guide -ROS 2 **Jazzy** on Ubuntu 24.04 Noble. This document covers everything beyond the basic install/launch flow in the top-level [`README.md`](../README.md): per-repository docs, all install/launch flags, dev mode, ports, environment overrides, and an overview of the packages dropped under `src/`. +ROS 2 **Jazzy** workspace for Lucy / InMoov. This guide covers developer mode, platform-specific setup, launch workflows, and pointers to deeper documentation. Basic install and launcher usage are in the top-level [`README.md`](../README.md). -## Cross-repository docs +## Developer mode -Maintainer documentation for each Lucy sub-repository is **owned per repository**: +Developer mode is toggled in **`Lucy.py`** (Install menu) or by setting **`DEV=true`** in `.env` (copy from [`.env.example`](../.env.example)). -| Repository | Developer documentation | -|------------|-------------------------| -| **lucy_ros_packages** | [`lucy_ros_packages/docs/DEVELOPER.md`](../src/lucy_ros_packages/docs/DEVELOPER.md) — bringup, `lucy_ros2_control`, `camera_ros`, CI; [**ros2_control on Lucy**](../src/lucy_ros_packages/doc/ROS2_CONTROL.md) | -| **inmoov_urdf** | [`inmoov_urdf/docs/DEVELOPER.md`](../src/inmoov_urdf/docs/DEVELOPER.md) — URDF/xacro, meshes, launches, RViz | +When enabled: -Repository-level READMEs: [`lucy_ros_packages`](../src/lucy_ros_packages/README.md), [`inmoov_urdf`](../src/inmoov_urdf/README.md). +| Behavior | Effect | +|----------|--------| +| **SSH clones** | `install.sh` uses `url_ssh` from [`config/repos.json`](../config/repos.json) instead of HTTPS | +| **No auto-launch** | Core and Control Panel are not started automatically on Launch | -## Packages dropped under `src/` by `install.sh` +SSH keys must be configured for GitHub on your host before running `./install.sh` with `DEV=true`. -- **inmoov_urdf** — InMoov URDF, RViz config, `control.launch.py`, `gazebo.launch.py`, `rviz_standalone.launch.py` (robot + viz; the web stack lives in `lucy_bringup`). -- **lucy_ros_packages** — `lucy_bringup`, `lucy_ros2_control`, `camera_ros`, etc. -- **lucy_control_panel** — Vite web app exposing the robot state and controls. +### Local overrides (gitignored) -The exact set of repositories, branches and clone URLs is in [`config/repos.json`](../config/repos.json). +| File | Purpose | +|------|---------| +| [`config/repos.json.local`](../config/repos.json.local) | Forks, feature branches, skip optional repos | +| [`config/launcher_config.json.local`](../config/launcher_config.json.local) | Custom Control Center package list (e.g. multi-robot) | +| [`config/install.profile.json`](../config/install.profile.json) | Windows installer choices (written by `Lucy-Setup.exe`) | -## `install.sh` - -The first run clones missing sub-repositories, builds the Docker image (`lucy_ros2:jazzy`), and runs `rosdep` + `colcon build --symlink-install` + `yarn install` inside the container. - -`--symlink-install` keeps `install/share//config/controllers.yaml` pointing at the **source tree** paths that `lucy_config_pipeline` writes (`src/inmoov_urdf/config/controllers.yaml`), so launch files and the pipeline stay aligned during iterative hardware edits. - -Subsequent runs fast-forward each clone to the branch declared in `config/repos.json` and rebuild the workspace. - -| Command | What it does | -|---------|--------------| -| `./install.sh` | Clone missing repos, pull existing ones, rebuild the workspace | -| `./install.sh --repair` | Wipe each repo under `src/` then re-clone and rebuild | -| `./install.sh --build-only` | Skip git; just rebuild the workspace inside the container | -| `./install.sh --arm[...]` | Build / run the image as `linux/arm64` (Apple Silicon under Docker Desktop). Persists in `.lucy-docker-platform`; combine with any other flag | - -### Apple Silicon notes - -Docker Desktop on Apple Silicon defaults to `linux/amd64` when no platform is pinned, which runs the container under emulation and can make `apt` / `rosdep` unreliable. Use `./install.sh --arm` to build and run a native `linux/arm64` image on `ubuntu:24.04` with `ros-jazzy-*` packages from apt (recorded in `.lucy-docker-platform`). - -### SSH vs HTTPS clones (`DEV=true`) - -`config/repos.json` carries both `url_https` (default) and `url_ssh` for each repo. To clone over SSH, copy `.env.example` to `.env` and set `DEV=true` before running `install.sh`. SSH keys must be configured for the relevant host. - -### Local repo overrides (`config/repos.json.local`) - -To point a repo at your own fork or a feature branch without editing the tracked `config/repos.json`, create **`config/repos.json.local`**. When present it is used instead of `repos.json` by both `install.sh` and the launcher (`windows/Lucy.py`), and it is gitignored so overrides are never committed. - -Use the same structure as `repos.json` — list only the repos you want to override (or all of them). Each entry needs `name` (the folder under `src/`), `branch`, and both `url_https` and `url_ssh` (Developer Mode selects SSH, otherwise HTTPS): +Example for a fork — same structure as `repos.json`: ```json { @@ -63,69 +38,189 @@ Use the same structure as `repos.json` — list only the repos you want to overr } ``` -Delete the file to fall back to the tracked `repos.json`. +For a multi-robot dev setup, copy [`config/launcher_config.json.local.example`](../config/launcher_config.json.local.example) to `launcher_config.json.local`. -### Windows install profile (`config/install.profile.json`) +## Platform setup -On Windows, **`Lucy-Setup.exe`** (or `Lucy.exe --cli …`) writes **`config/install.profile.json`** (gitignored) to record install choices: `lucy_ws` version, `repos_branch` (default `master`), `fetch_method` (`git` or `zip`), and whether **developer install** was selected. The file is created automatically on first install. +### Linux -| Windows | Linux/macOS equivalent | -|---------|------------------------| +Standard path: `./install.sh` then `python3 Lucy.py`. + +**Wayland:** RViz/Gazebo may need `xhost +local:` or an X11 session. + +**NixOS:** Pixi/RoboStack needs host GL libraries prepended **and** Mesa EGL — both are applied by [`scripts/nix_gl_env.sh`](../scripts/nix_gl_env.sh) (launcher and `pixi run sim*`). Install **`nixGLIntel`** (or another nixGL wrapper) on PATH, or rely on the `/run/opengl-driver/lib` fallback. **Do not set `LUCY_NIX_GL=0`** — EGL/`GZ_IP` alone is not enough; sim will hang on "requesting world names". Optional overrides: `LUCY_NIX_GL_WRAPPER`, `__EGL_VENDOR_LIBRARY_FILENAMES`, `GZ_IP` — see `.env.example`. + +### macOS + +- Install **tmux** (`brew install tmux`) for the multi-window launcher. +- **Port 5000** is often used by AirPlay Receiver. Disable it in **System Settings → General → AirDrop & Handoff**, or set `PORT_CONTROL_PANEL=5001` in `.env`. +- Pixi uses Cyclone DDS on macOS (`RMW_IMPLEMENTATION=rmw_cyclonedds_cpp` in `pixi.toml`). + +### Windows + +End-user install: **`Lucy-Setup.exe`** → **`Lucy.exe`**. Full details: [`windows/README.md`](../windows/README.md). + +Developer CLI equivalents: + +| Windows | Linux/macOS | +|---------|-------------| | `Lucy-Setup.exe` → Fresh install | `./install.sh` | -| `Lucy-Setup.exe` → Update | `./install.sh` / `./install.sh --update` | +| `Lucy-Setup.exe` → Update | `./install.sh` | | `Lucy-Setup.exe` → Repair | `./install.sh --repair` | -| `Lucy.exe` (no args) | `./launch_lucy.sh` / **Launch** in `Lucy.py` | +| `Lucy.exe` | `./launch_lucy.sh` | | `Lucy.exe --cli build-only` | `./install.sh --build-only` | -## `launch_lucy.sh` +Launch runs via Git Bash (`bash launch_lucy.sh`). Without tmux, the Control Center runs directly (`pixi run -- python launcher.py`). -Builds the Docker image if needed, mounts the workspace at `/workspace`, sources the built ROS overlay, then: +### Workspace install (`install.sh`) -- **Normal mode (default)** — starts the control panel (Vite) in the background and runs `ros2 launch lucy_bringup lucy.launch.py gazebo:=true rviz:=true` in the foreground. GUI / X11 forwarded automatically when available. -- **Dev mode (`DEV=true` in env or `.env`)** — same control panel in the background, but drops you into an interactive Jazzy shell so you can run any `ros2 launch` yourself (the script prints typical commands). +Pixi installs RoboStack Jazzy; `colcon build --symlink-install` builds `src/`; `yarn install` sets up the control panel. | Command | What it does | |---------|--------------| -| `./launch_lucy.sh` | Default launch (Control Panel + RViz + Gazebo, or dev shell when `DEV=true`) | -| `./launch_lucy.sh --headless` | Same flow without GUI / X11 (Gazebo runs headless, RViz is disabled) | -| `./launch_lucy.sh ` | Run a single command in the container — no control panel, no auto-launch | -| `DEV=true ./launch_lucy.sh` | Force dev mode for one run | +| `./install.sh` | Clone missing repos, pull existing ones, `pixi install`, colcon build | +| `./install.sh --repair` | Wipe each repo under `src/` then re-clone and rebuild | +| `./install.sh --build-only` | Skip git; `pixi install` + colcon + panel yarn | +| `./install.sh --skip-build` | Clone/pull only (CI) | -### `ros2 launch` cheat sheet (dev mode) +**Do not use `rosdep`** — it bypasses Pixi/RoboStack. Add deps via `pixi.toml` or clone into `src/`. See [`docs/pixi_setup.md`](pixi_setup.md). -Run these inside the dev-mode shell — `lucy_bringup` already brings up `rosbridge` and the `/config/*` services via `web_ros_api`: +**RealSense** (optional, not in Pixi): after a normal build, run `./scripts/build_local_realsense.sh` or `LUCY_BUILD_REALSENSE=1 ./install.sh`. Primary target is Linux; see script for aarch64 notes. -| What you want | Command | -|---------------|---------| -| Default Jetson stack (no RViz / no Gazebo) | `ros2 launch lucy_bringup lucy.launch.py real:=true` | -| Jetson + RViz | `ros2 launch lucy_bringup lucy.launch.py real:=true rviz:=true` | -| Dev + panel + RViz (no micro-ROS / cameras) | `ros2 launch lucy_bringup lucy.launch.py rviz:=true` | -| Gazebo sim + panel (`rviz:=false` = headless Gazebo) | `ros2 launch lucy_bringup lucy.launch.py gazebo:=true` | +**Packages under `src/`** (from [`config/repos.json`](../config/repos.json)): -`gazebo:=true` cannot be combined with `real:=true` (the launch aborts). With Gazebo, `rviz` maps to `start_rviz` in `inmoov_urdf/gazebo.launch.py`. +| Repo | Role | +|------|------| +| **inmoov_urdf** | URDF, Gazebo/RViz launches | +| **lucy_ros_packages** | `lucy_bringup`, ros2_control, cameras | +| **lucy_control_panel** | Web UI | +| **micro_ros_agent**, **audio_common** | Optional (`optional: true` in repos.json) | -### SIMULATION ONLY + RELOAD (control panel) +## Launch -From **Configuration → ACTIVATE**, enable **SIMULATION ONLY** to run **VALIDATE → ACTIVATE → RELOAD** without BUILD/FLASH. The pipeline generates a single mock `ros2_control` block and `lucy_sim_controller`, installs `inmoov_ros2_control.xacro` + `controllers.yaml` into the source robot tree, then calls **`/lucy_control/restart`** (`lucy_control_supervisor`) to restart `robot_state_publisher`, `ros2_control_node` (RViz-only), and controller spawners. +Two supported workflows. -Hardware mode runs the same **RELOAD** step after BUILD/FLASH once ros2_control artifacts are regenerated. +### 1. Control Center (recommended) -**Gazebo caveat:** spawners and RSP can be restarted without relaunching the world; if you add/remove joints in the URDF hardware blocks, `gz_ros2_control` may require a full Gazebo restart — the supervisor response notes this when `use_gazebo_sim:=true`. +Day-to-day use: one tmux session (Linux/macOS), toggle components in the TUI. -## Ports and environment overrides +| Entry | Command | +|-------|---------| +| TUI manager | `python3 Lucy.py` → **Launch** | +| Direct | `./launch_lucy.sh` | +| Windows | `Lucy.exe` | -| Env var | Default | Purpose | -|---------|---------|---------| -| `DEV` | unset | `true` → use `url_ssh` in `repos.json` (install) and the interactive dev shell (launch) | -| `PORT_CONTROL_PANEL` | matches container port | Host port the control panel is published on | -| `PORT_CONTROL_PANEL_CONTAINER` | `VITE_PORT` from `src/lucy_control_panel/.env`, else `4004` | Port the Vite dev server listens on inside the container | -| `DOCKER_GUI_DISPLAY` | host `$DISPLAY` | X display string passed to the container (use when the host `DISPLAY` doesn't reach Docker, e.g. Docker Desktop) | -| `DOCKER_GUI_USE_HOST_NETWORK` | unset | Run with `--network=host` and `DISPLAY=:0` (alternative GUI path) | -| `LUCY_DOCKER_PLATFORM` | content of `.lucy-docker-platform`, else host CPU | Docker `--platform` to build/run with (e.g. `linux/arm64`) | -| `LUCY_INSTALL_SKIP_XHOST` | unset | Skip the `xhost` requirement in `install.sh` (set automatically when `CI=true`) | +| `launch_lucy.sh` flag | Purpose | +|-----------------------|---------| +| *(none)* | tmux + Control Center | +| `--headless ` | One-shot command (default: `ros2 doctor --report`) | +| `--shell` | Interactive dev shell (`pixi run shell`) | + +The launcher wraps panes in `pixi run`, forwards display/GL env from `.env`, and sources `nix_gl_env.sh` for `ros2 launch`. Package list: [`docs/launcher_packages.md`](launcher_packages.md). + +### 2. Pixi component tasks -Inside the container, Vite proxies `/rosbridge` to `ws://127.0.0.1:9090`, and rosbridge is published on host port `9090`. +Run stacks or tools in **separate terminals** (debug / scripting). ROS tasks use [`scripts/pixi_lucy_launch.sh`](../scripts/pixi_lucy_launch.sh). -## Docker image rebuilds +| Task | Command | What it starts | +|------|---------|----------------| +| Core | `pixi run core` | `lucy_bringup` (rosbridge, config pipeline, mock stack) | +| Gazebo (GUI) | `pixi run sim` | Core + Gazebo window | +| Gazebo headless | `pixi run sim-headless` | Core + Gazebo server only | +| Sim + RViz | `pixi run sim-rviz` | Headless Gazebo + RViz | +| RViz only | `pixi run rviz` | Core + RViz | +| Control panel | `pixi run control-panel` | Vite dev server | +| rqt | `pixi run rqt` | rqt (needs core running) | +| Lucy CLI | `pixi run lucy-cli` | `ros2 run lucy_cli tui` (needs core) | -`docker/ensure_image.sh` stamps each built image with `LABEL lucy.dockerfile.sha256="|"`. Both `install.sh` and `launch_lucy.sh` rebuild the image when the label no longer matches the current `Dockerfile.jazzy` + target platform. +**Robot package** (default `inmoov_urdf`): + +```bash +LUCY_ROBOT_PACKAGE=thais_urdf pixi run sim-headless +``` + +Example multi-terminal sim: + +```bash +pixi run sim-headless # terminal 1 +pixi run control-panel # terminal 2 +``` + +### Debug shell + +Interactive RoboStack + colcon overlay for ROS CLI: + +```bash +pixi run shell +# or: ./launch_lucy.sh --shell +``` + +```bash +ros2 topic list +ros2 service list +ros2 launch lucy_bringup lucy.launch.py rviz:=true +``` + +For manual Gazebo launches outside `pixi run sim*`, source GL env when needed (e.g. NixOS with `nixGLIntel`): + +```bash +source scripts/nix_gl_env.sh +ros2 launch lucy_bringup lucy.launch.py gazebo:=true +``` + +### TUI ↔ Pixi CLI + +| Action | Command | +|--------|---------| +| Install / update | `./install.sh` | +| Rebuild | `pixi run build` then `pixi run panel-install` | +| Launch | `./launch_lucy.sh` | +| Dev shell | `pixi run shell` | +| Headless one-shot | `./launch_lucy.sh --headless ros2 doctor --report` | +| Clean build dirs | `pixi run clean` | +| Workspace tests | `pixi run workspace-test` | + +### `ros2 launch` modifiers + +`lucy_bringup` brings up rosbridge and `/config/*` via `web_ros_api`: + +| Goal | Command | +|------|---------| +| Default dev stack (mock hardware) | `ros2 launch lucy_bringup lucy.launch.py` | +| Jetson / real hardware | `… real:=true` | +| Real + RViz | `… real:=true rviz:=true` | +| Gazebo headless | `pixi run sim-headless` or `… gazebo:=true headless:=true` | + +`gazebo:=true` cannot be combined with `real:=true`. With Gazebo, `rviz` is forwarded to the robot package's `gazebo.launch.py`. + +### Control panel: SIMULATION ONLY + RELOAD + +From **Configuration → ACTIVATE**, enable **SIMULATION ONLY** to run **VALIDATE → ACTIVATE → RELOAD** without BUILD/FLASH. The pipeline writes mock ros2_control artifacts and calls **`/lucy_control/restart`**. Hardware mode runs the same **RELOAD** after BUILD/FLASH. + +**Gazebo caveat:** joint changes in URDF hardware blocks may require a full Gazebo restart when `use_gazebo_sim:=true`. + +## Ports and environment + +| Env var | Default | Purpose | +|---------|---------|---------| +| `DEV` | unset | `true` → SSH clones during `install.sh` | +| `PORT_CONTROL_PANEL` | auto | Host port for control panel URL | +| `PORT_CONTROL_PANEL_CONTAINER` | `VITE_PORT` from `src/lucy_control_panel/.env`, else `4004` | Port the Vite dev server listens on inside the container | +| `PORT_ROSBRIDGE` | `9090` | rosbridge WebSocket | +| `LUCY_ROBOT_PACKAGE` | `inmoov_urdf` | Robot package for `pixi run core` / `sim-*` | +| `LUCY_NIX_GL` | `auto` | Set `0` to skip host GL prepend (**breaks NixOS Gazebo sim**) | +| `LUCY_NIX_GL_WRAPPER` | auto | `nixGLIntel`, `nixGLDefault`, or `nixGL` | + +Vite proxies `/rosbridge` to `ws://127.0.0.1:9090`. Launcher sets `LUCY_LCP_*` vars for panel URLs — see [`launch_lucy.sh`](../launch_lucy.sh). + +## More + +| Document | Contents | +|----------|----------| +| [`docs/launcher_packages.md`](launcher_packages.md) | Adding packages to the Control Center | +| [`docs/pixi_setup.md`](pixi_setup.md) | Pixi/RoboStack deps, lock workflow, component tasks | +| [`docs/pixi_release.md`](pixi_release.md) | Release packaging (pixi-build-ros) | +| [`windows/README.md`](../windows/README.md) | Windows installer and `Lucy.exe` | +| [`src/lucy_ros_packages/docs/DEVELOPER.md`](../src/lucy_ros_packages/docs/DEVELOPER.md) | bringup, ros2_control, CI | +| [`src/lucy_ros_packages/doc/ROS2_CONTROL.md`](../src/lucy_ros_packages/doc/ROS2_CONTROL.md) | ros2_control on Lucy | +| [`src/inmoov_urdf/docs/DEVELOPER.md`](../src/inmoov_urdf/docs/DEVELOPER.md) | URDF, meshes, sim launches | diff --git a/docs/launcher_packages.md b/docs/launcher_packages.md index 94a1911..937982d 100644 --- a/docs/launcher_packages.md +++ b/docs/launcher_packages.md @@ -44,10 +44,12 @@ Every package entry in `config/launcher_config.json` uses the following fields t When you run `./launch_lucy.sh`: -1. It builds and enters the Docker container. -2. It drops you into a **tmux** session named `lucy_ws`. -3. It automatically runs `launcher.py` (the TUI) in the main window. +1. It ensures the workspace is built (`install/setup.bash` exists). +2. On Linux/macOS with **host tmux**, it starts or attaches to a **tmux** session named `lucy_ws`. +3. The main window runs `pixi run -- python launcher.py` (Control Center TUI). + +Package commands in other tmux windows are wrapped in `pixi run` so each pane gets RoboStack and the colcon overlay. Display and OpenGL-related variables from your shell and `.env` are forwarded into those panes. When you apply changes in `launcher.py`: - **Core + Modifiers:** The script takes the core command, appends all active modifier commands, and spins up a dedicated `core` tmux window. -- **Interfaces / Tools:** The script spins up a new tmux window named after the package's `id` and executes its command. Alternatively, if a complex command object is provided, it executes the explicit `start` and `stop` shell commands in the background. +- **Interfaces / Tools:** The script spins up a new tmux window named after the package's `id` and executes its command via Pixi. Legacy complex `{start, stop, is_running}` objects are still supported for local overrides. diff --git a/docs/pixi_release.md b/docs/pixi_release.md new file mode 100644 index 0000000..698b053 --- /dev/null +++ b/docs/pixi_release.md @@ -0,0 +1,30 @@ +# Pixi release infrastructure (follow-up PR) + +This document outlines the **deferred** end-user release path. The PoC branch keeps day-to-day development on **colcon + Pixi activation**; release packaging is a separate PR after validation gates are green. + +## Scope + +Full-stack artifact (not `lucy_bringup` alone): + +- All `lucy_ros_packages/*`, `inmoov_urdf`, optional `thais_urdf` +- External clones built in CI (`micro_ros_agent`, `audio_common`) +- Launcher assets + control panel packaging strategy +- Release CI manifest with `preview = ["pixi-build"]` only in the release workflow + +## Distribution options + +| Option | Best for | +|--------|----------| +| Private conda channel (prefix.dev) | Networked workstations | +| Pixi Pack bundles | Jetson / air-gapped | + +## Suggested follow-up commits + +1. `chore(pixi): add per-package pixi.toml stubs for release builds` +2. `ci(release): add tag-triggered workflow with pixi-build-ros` +3. `docs(pixi): document end-user install from channel or Pixi Pack` + +## References + +- [pixi-build-ros](https://github.com/RoboStack/pixi-build-ros) +- Dev workflow: [`docs/pixi_setup.md`](pixi_setup.md) diff --git a/docs/pixi_setup.md b/docs/pixi_setup.md new file mode 100644 index 0000000..1efc3f7 --- /dev/null +++ b/docs/pixi_setup.md @@ -0,0 +1,100 @@ +# Pixi + RoboStack setup + +Lucy uses [Pixi](https://pixi.prefix.dev/) with the [RoboStack Jazzy](https://robostack.github.io/) channel for ROS 2 Jazzy dependencies. Workspace packages under `src/` are built with **colcon** into `install/`; Pixi provides the base ROS environment and activation scripts. + +## Do not use rosdep + +**Do not run `rosdep install`** in this workspace. rosdep resolves system packages via apt and bypasses Pixi, which breaks the reproducible RoboStack environment. + +Instead: + +- Add missing ROS packages with `pixi add --feature ros ros-jazzy-` (or edit `[feature.ros.dependencies]` in `pixi.toml`). +- Clone packages that are not on RoboStack into `src/` via [`config/repos.json`](../config/repos.json). + +## Dependency audit workflow + +When adding or changing workspace packages: + +1. Read `package.xml` / `CMakeLists.txt` for new `depend` / `find_package` entries. +2. Check whether RoboStack provides `ros-jazzy-` on [prefix.dev/robostack-jazzy](https://prefix.dev/robostack-jazzy). +3. If available, add to `pixi.toml` under `[feature.ros.dependencies]`. +4. If not available (e.g. `micro_ros_agent`, `audio_common`), add a clone entry to `config/repos.json` (use `"optional": true` when the stack works without it). +5. Regenerate the lock and install: + +```bash +pixi install # updates pixi.lock for every platform in pixi.toml +``` + +`install.sh` runs `pixi lock` automatically if `pixi.lock` is missing, then `pixi install`. + +**Pixi ≥ 0.78** is recommended for multi-platform lock resolution (`curl -fsSL https://pixi.sh/install.sh | bash`). + +## RoboStack vs source clones + +| Approach | When to use | +|----------|-------------| +| **Pixi / RoboStack** | Standard ROS Jazzy packages (`ros-jazzy-desktop`, `ros-jazzy-ros-gz`, controllers, rosbridge, etc.) | +| **Clone to `src/`** | Packages not on RoboStack, forks, or workspace-specific repos (`lucy_ros_packages`, `inmoov_urdf`) | +| **Optional `src/` clones** | `micro_ros_agent`, `audio_common` — not on RoboStack; cloned by default for release/CI parity but **not required** for core bringup or the control panel. Entries carry `"optional": true` in `repos.json`; omit from `repos.json.local` if you do not need them | +| **Local build** | RealSense — not in Pixi; run `./scripts/build_local_realsense.sh` **after** a normal `pixi run build` (or `LUCY_BUILD_REALSENSE=1 ./install.sh` at end of install) | + +### Mutex pin + +`ros2-distro-mutex = "0.15.*"` in `pixi.toml` keeps all RoboStack packages on the same rebuild cycle ([RoboStack #125](https://github.com/RoboStack/ros-jazzy/issues/125)). + +### Platform-specific deps + +Use Pixi target tables in `pixi.toml`: + +- `[target.linux]` — `gstreamer`, `libgl-devel` +- **tmux** — host package (apt, Homebrew); not in Pixi +- `[feature.ros.target.osx-*]` — `pygraphviz`, Cyclone DDS RMW + +## Build, launch, and activation + +| Task | Command | +|------|---------| +| Install env | `pixi install` | +| Build workspace | `pixi run build` | +| Control panel deps | `pixi run panel-install` | +| Tests | `pixi run test` | +| ROS doctor | `pixi run doctor` | +| Dev shell (ROS CLI) | `pixi run shell` | + +### Launch component tasks + +Run in **separate terminals** (or use `./launch_lucy.sh` Control Center instead). ROS stacks use [`scripts/pixi_lucy_launch.sh`](../scripts/pixi_lucy_launch.sh). + +| Task | Command | +|------|---------| +| Core | `pixi run core` | +| Gazebo GUI | `pixi run sim` | +| Gazebo headless | `pixi run sim-headless` | +| Sim + RViz | `pixi run sim-rviz` | +| RViz | `pixi run rviz` | +| Control panel | `pixi run control-panel` | +| rqt | `pixi run rqt` | +| Lucy CLI | `pixi run lucy-cli` | + +Robot package: `LUCY_ROBOT_PACKAGE=thais_urdf pixi run sim-headless` (default `inmoov_urdf`). + +See [`docs/developer_lucy_packages.md`](developer_lucy_packages.md#launch) for Control Center vs Pixi workflows and the debug shell. + +Colcon uses `--symlink-install` on Linux/macOS and `--merge-install` on Windows (`pixi.toml` `[feature.build]` tasks). + +Activation scripts (`install/setup.bash` or `install/setup.bat`) are wired via `[target.unix.activation]` / `[target.win.activation]` in `pixi.toml`, so `pixi run` and `pixi shell` automatically overlay the workspace. `GZ_SIM_SYSTEM_PLUGIN_PATH` points at conda Gazebo plugins (`lib` on Unix, `Library/bin` on Windows). + +## Lock refresh checklist + +After editing `pixi.toml`: + +```bash +pixi install +git add pixi.toml pixi.lock +``` + +Commit both files together so CI and other platforms stay in sync. + +## Release builds (follow-up) + +End-user **pre-built** packages via `pixi-build-ros` and conda channels are planned as a separate release workflow. See [`docs/pixi_release.md`](pixi_release.md). diff --git a/scripts/build_local_realsense.sh b/scripts/build_local_realsense.sh new file mode 100755 index 0000000..f1d6148 --- /dev/null +++ b/scripts/build_local_realsense.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Build Intel librealsense and realsense-ros locally (not via Pixi/RoboStack). +# +# RoboStack does not publish ros-jazzy-realsense2-camera reliably across platforms +# (especially linux-aarch64). Use this script when you need RealSense +# hardware support. camera_ros (MJPEG/GStreamer) does not require this. +# +# Usage: +# ./scripts/build_local_realsense.sh # default prefix: .local/realsense +# LUCY_REALSENSE_PREFIX=/opt/realsense ./scripts/build_local_realsense.sh +# +# Run after a normal workspace build (install/setup.bash must exist). Does not +# replace pixi run build — it adds librealsense + realsense-ros to install/. +# install.sh runs this when LUCY_BUILD_REALSENSE=1 (after colcon + panel-install). +# +# Uses a portable CPU count for cmake -j (nproc on Linux, sysctl on macOS). + +set -euo pipefail + +parallel_jobs() { + nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PREFIX="${LUCY_REALSENSE_PREFIX:-${WORKSPACE_ROOT}/.local/realsense}" +LIBRS_TAG="${LUCY_LIBRS_TAG:-v2.56.2}" +REALSENSE_ROS_BRANCH="${LUCY_REALSENSE_ROS_BRANCH:-ros2-master}" +BUILD_DIR="${WORKSPACE_ROOT}/.local/build/realsense" +SRC_DIR="${WORKSPACE_ROOT}/.local/src" + +mkdir -p "$BUILD_DIR" "$SRC_DIR" "$PREFIX" + +build_librealsense() { + local librs_dir="${SRC_DIR}/librealsense" + if [ ! -d "$librs_dir/.git" ]; then + echo "Cloning librealsense (${LIBRS_TAG}) ..." + git clone --depth 1 --branch "$LIBRS_TAG" https://github.com/IntelRealSense/librealsense.git "$librs_dir" + else + echo "Using existing librealsense clone at $librs_dir" + fi + + cmake -S "$librs_dir" -B "${BUILD_DIR}/librealsense" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$PREFIX" \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_GRAPHICAL_EXAMPLES=OFF \ + -DBUILD_WITH_OPENMP=OFF + + cmake --build "${BUILD_DIR}/librealsense" -j"$(parallel_jobs)" + cmake --install "${BUILD_DIR}/librealsense" +} + +build_realsense_ros() { + local rs_ros_dir="${SRC_DIR}/realsense-ros" + if [ ! -d "$rs_ros_dir/.git" ]; then + echo "Cloning realsense-ros (${REALSENSE_ROS_BRANCH}) ..." + git clone --depth 1 --branch "$REALSENSE_ROS_BRANCH" https://github.com/IntelRealSense/realsense-ros.git "$rs_ros_dir" + else + echo "Using existing realsense-ros clone at $rs_ros_dir" + fi + + export CMAKE_PREFIX_PATH="${PREFIX}:${CMAKE_PREFIX_PATH:-}" + export LD_LIBRARY_PATH="${PREFIX}/lib:${LD_LIBRARY_PATH:-}" + + # Build into workspace overlay (requires ROS env from pixi shell / install/setup.bash). + if [ -f "${WORKSPACE_ROOT}/install/setup.bash" ]; then + # shellcheck disable=SC1091 + source "${WORKSPACE_ROOT}/install/setup.bash" + elif [ -n "${ROS_DISTRO:-}" ]; then + # shellcheck disable=SC1091 + source "/opt/ros/${ROS_DISTRO}/setup.bash" + else + echo "ROS environment not found. Run from pixi shell or after colcon build." >&2 + exit 1 + fi + + colcon build --symlink-install \ + --paths "$rs_ros_dir/realsense2_camera_msgs" "$rs_ros_dir/realsense2_camera" \ + --cmake-args -DCMAKE_PREFIX_PATH="$PREFIX" +} + +echo "Local RealSense build — install prefix: $PREFIX" +build_librealsense +build_realsense_ros +echo "Done. Prefix: $PREFIX" diff --git a/windows/Lucy.py b/windows/Lucy.py index 3962d31..59b7dd6 100644 --- a/windows/Lucy.py +++ b/windows/Lucy.py @@ -1,14 +1,16 @@ # Windows launcher for the Lucy workspace. # -# Compiled to Lucy.exe via PyInstaller. Default behaviour: start the workspace -# (Docker + tmux + launcher). Install/update/repair is handled by Lucy-Setup.exe -# via the hidden --cli mode (see windows/install_runner.py). +# Compiled to Lucy.exe via PyInstaller. Default behaviour: launch via Pixi +# (native RoboStack + Control Center launcher). Install/update/repair is handled +# by Lucy-Setup.exe via the hidden --cli mode (see windows/install_runner.py). # # PREREQUISITES: -# 1. Docker Desktop for Windows (must be running) -# 2. Workspace must be installed (run Lucy-Setup.exe first) +# 1. Pixi — https://pixi.prefix.dev/latest/installation/ +# 2. Git Bash (runs launch_lucy.sh) — https://git-scm.com/install/windows +# 3. Workspace installed (run Lucy-Setup.exe first) import os +import shutil import subprocess import sys @@ -20,20 +22,12 @@ if _WINDOWS_DIR not in sys.path: sys.path.insert(0, _WINDOWS_DIR) -import install_ops # noqa: E402 - -if getattr(sys, 'frozen', False): +if getattr(sys, "frozen", False): PROJECT_ROOT = os.path.dirname(sys.executable) else: PROJECT_ROOT = os.path.dirname(_WINDOWS_DIR) -IMAGE_NAME = install_ops.IMAGE_NAME -WORKSPACE_DIR_HOST = PROJECT_ROOT -WORKSPACE_DIR_CONTAINER = install_ops.WORKSPACE_CONTAINER - -LCP_DEFAULT_PORT = 4004 - -_CLI_MODES = frozenset(('install', 'update', 'repair', 'build-only', 'check-prereqs')) +_CLI_MODES = frozenset(("install", "update", "repair", "build-only", "check-prereqs")) def run_command(command, check=True, interactive=False): @@ -41,9 +35,15 @@ def run_command(command, check=True, interactive=False): print(f"--- Running: {' '.join(command)} ---") try: if interactive: - return subprocess.run(command, check=check).returncode - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - for line in iter(process.stdout.readline, ''): + return subprocess.run(command, check=check, cwd=PROJECT_ROOT).returncode + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=PROJECT_ROOT, + ) + for line in iter(process.stdout.readline, ""): print(line.rstrip()) process.wait() if check and process.returncode != 0: @@ -57,170 +57,56 @@ def run_command(command, check=True, interactive=False): return e.returncode -def _read_env_value(env_path, key): - if not os.path.exists(env_path): - return None - value = None - try: - with open(env_path, 'r') as f: - for line in f: - stripped = line.strip() - if stripped.startswith('#') or '=' not in stripped: - continue - if stripped.startswith(f"{key}="): - value = stripped.split('=', 1)[1].strip().strip('"').strip("'") - except OSError: - return None - return value - - -def _read_lcp_env_value(key): - return _read_env_value(os.path.join(PROJECT_ROOT, 'src', 'lucy_control_panel', '.env'), key) - - -def _read_root_env_value(key): - return _read_env_value(os.path.join(PROJECT_ROOT, '.env'), key) - - -def _lcp_container_port(): - val = _read_lcp_env_value('VITE_PORT') - if val and val.isdigit(): - return int(val) - return LCP_DEFAULT_PORT - - -def _lcp_scheme(): - val = _read_lcp_env_value('VITE_HTTPS') - if val and val.strip().lower() == 'true': - return 'https' - return 'http' - - -def _docker_gui_args(): - gui_display = os.environ.get('DOCKER_GUI_DISPLAY', os.environ.get('DISPLAY', '')).strip() - if sys.platform == 'win32' and not gui_display: - gui_display = 'host.docker.internal:0' - - if not gui_display: - return [] - - args = ['-e', f'DISPLAY={gui_display}', '-e', 'QT_X11_NO_MITSHM=1'] - - if os.environ.get('DOCKER_GUI_USE_HOST_NETWORK'): - if sys.platform == 'win32': - print("WARNING: DOCKER_GUI_USE_HOST_NETWORK is not supported on Windows; using DISPLAY only.") - return args - return ['--network=host', '-e', 'DISPLAY=:0', '-e', 'QT_X11_NO_MITSHM=1'] - - if sys.platform == 'win32': - if 'host.docker.internal' in gui_display: - args.extend(['--add-host', 'host.docker.internal:host-gateway']) - return args - - return args + ['-v', '/tmp/.X11-unix:/tmp/.X11-unix:rw'] - - -def _parse_display_host_port(display_value): - if display_value.startswith(':'): - return 'localhost', 6000 + int(display_value[1:].split('.')[0]) - host, _, display_str = display_value.rpartition(':') - if not host: - host = 'localhost' - try: - display_num = int(display_str) - except ValueError: - display_num = 0 - return host, 6000 + display_num - - -def _docker_gui_diagnostics(gui_display, gui_args): - print("--- GUI diagnostics ---") - print(f"DISPLAY value used inside the container: {gui_display}") - host, port = _parse_display_host_port(gui_display) - print(f"Checking TCP connectivity to X server at {host}:{port}...") - - python_check = ( - "import os, socket, sys\n" - "display = os.environ.get('DISPLAY', '')\n" - "print('container DISPLAY=' + display)\n" - f"host = '{host}'\n" - f"port = {port}\n" - "try:\n" - " s = socket.create_connection((host, port), timeout=3)\n" - " print('OK: connected to', host, port)\n" - " s.close()\n" - "except Exception as e:\n" - " print('FAIL: could not connect to', host, port, e)\n" - " sys.exit(1)\n" +def _workspace_built(): + install_dir = os.path.join(PROJECT_ROOT, "install") + return ( + os.path.isfile(os.path.join(install_dir, "setup.bat")) + or os.path.isfile(os.path.join(install_dir, "setup.bash")) ) - docker_cmd = ['docker', 'run', '--rm'] + install_ops.docker_run_platform_args(PROJECT_ROOT) + gui_args + [IMAGE_NAME, '-c', f'python3 -c "{python_check}"'] - run_command(docker_cmd, check=False) - def launch_workspace(): - """Start Docker, attach to the Lucy Control Center launcher inside tmux.""" - if not os.path.isfile(os.path.join(PROJECT_ROOT, 'install', 'setup.bash')): - print("Workspace not built. Run Lucy-Setup.exe to install or update first.", file=sys.stderr) + """Start Pixi and attach to the Lucy Control Center launcher.""" + if not _workspace_built(): + print( + "Workspace not built. Run Lucy-Setup.exe to install or update first.", + file=sys.stderr, + ) sys.exit(1) - print("Launching workspace...") + if shutil.which("pixi") is None: + print( + "Missing pixi. Install: https://pixi.prefix.dev/latest/installation/", + file=sys.stderr, + ) + sys.exit(1) - container_script = ( - "source /opt/ros/jazzy/setup.bash && " - "cd /workspace && source install/setup.bash && " - "tmux start-server && " - "if ! tmux has-session -t lucy_ws 2>/dev/null; then " - "tmux new-session -d -s lucy_ws -n 'Lucy Workspace' 'python3 /workspace/launcher.py'; " - "fi && " - "tmux attach-session -t lucy_ws" - ) + bash = shutil.which("bash") + launch_script = os.path.join(PROJECT_ROOT, "launch_lucy.sh") + if not bash: + print( + "Git Bash (bash) is required to run launch_lucy.sh on Windows.", + file=sys.stderr, + ) + print("Install Git for Windows: https://git-scm.com/install/windows", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(launch_script): + print(f"Missing launch script: {launch_script}", file=sys.stderr) + sys.exit(1) - volume_mapping = install_ops.format_volume_mapping(WORKSPACE_DIR_HOST, WORKSPACE_DIR_CONTAINER) - gui_args = _docker_gui_args() - display_value = os.environ.get('DOCKER_GUI_DISPLAY', os.environ.get('DISPLAY', '')) - if sys.platform == 'win32' and not display_value: - display_value = 'host.docker.internal:0' - - if gui_args: - print(f"Enabling GUI forwarding with DISPLAY={display_value}") - _docker_gui_diagnostics(display_value, gui_args) - else: - print("No DISPLAY configured; running without GUI.") - - lcp_container_port = _lcp_container_port() - lcp_host_port = lcp_container_port - lcp_scheme = _lcp_scheme() - - val = _read_root_env_value('PORT_ROSBRIDGE') - rosbridge_host_port = int(val) if val and val.isdigit() else 9090 - - docker_cmd = [ - 'docker', 'run', '-it', '--rm', - *install_ops.docker_run_platform_args(PROJECT_ROOT), - '--name', 'lucy_dev_win', - '-p', f'{rosbridge_host_port}:9090', - '-p', f'{lcp_host_port}:{lcp_container_port}', - '-v', volume_mapping, - '-e', f'LUCY_LCP_PUBLISHED_HOST_PORT={lcp_host_port}', - '-e', f'LUCY_LCP_CONTAINER_PORT={lcp_container_port}', - '-e', f'LUCY_LCP_SCHEME={lcp_scheme}', - ] + gui_args + [ - IMAGE_NAME, - '-c', container_script - ] - - run_command(docker_cmd, interactive=True) + print("Launching workspace...") + run_command([bash, launch_script], interactive=True) def _is_cli_invocation(): - return len(sys.argv) > 1 and (sys.argv[1] == '--cli' or sys.argv[1] in _CLI_MODES) + return len(sys.argv) > 1 and (sys.argv[1] == "--cli" or sys.argv[1] in _CLI_MODES) def _run_cli(): """Install/update/repair — used by Lucy-Setup.exe, not exposed in the default UX.""" from install_runner import main as install_main - argv = [a for a in sys.argv[1:] if a != '--cli'] + + argv = [a for a in sys.argv[1:] if a != "--cli"] return install_main(argv) diff --git a/windows/README.md b/windows/README.md index 179a41a..dbc0f4e 100644 --- a/windows/README.md +++ b/windows/README.md @@ -5,7 +5,7 @@ On Windows, Lucy is split into two programs: | Program | Purpose | |---------|---------| | **`Lucy-Setup.exe`** | Install, update, repair, pick version, developer mode | -| **`Lucy.exe`** | Launch the workspace (Docker → Lucy Control Center) | +| **`Lucy.exe`** | Launch the workspace (Pixi → Control Center) | `windows/Lucy.py` is the PyInstaller source for `Lucy.exe`. It launches the workspace directly — there is no install menu. Use **`Lucy-Setup.exe`** for all install lifecycle tasks. @@ -13,22 +13,17 @@ On Windows, Lucy is split into two programs: Install the following before running the project. After each installation, close and reopen any terminal so the updated `PATH` is picked up. -1. **Docker Desktop**: Download from [docs.docker.com/desktop/setup/install/windows-install](https://docs.docker.com/desktop/setup/install/windows-install/). - - During installation, **uncheck "Use WSL 2 instead of Hyper-V"** unless you are an advanced/dev user who specifically needs the WSL 2 backend. - - After install, **start Docker Desktop** and wait until it reports "running" before launching Lucy. -2. **Git for Windows** (optional but recommended): Download from [git-scm.com/install/windows](https://git-scm.com/install/windows). - - Without Git, the installer downloads sub-repositories as ZIP archives. -3. **Python 3** (manual dev workflow only): Download from [python.org/downloads](https://www.python.org/downloads/). -4. **Windows X server** (optional): Required for GUI apps such as `rqt` inside the Docker container. - - We recommend [VcXsrv](https://github.com/marchaesen/vcxsrv/releases). - - Start VcXsrv on display `0`, allow TCP connections, and disable access control if needed. - - Make sure Windows Firewall allows port `6000`. +1. **Pixi** — [pixi.prefix.dev/latest/installation](https://pixi.prefix.dev/latest/installation/) (≥ 0.78 recommended). +2. **Git for Windows** — [git-scm.com/install/windows](https://git-scm.com/install/windows). + - Required for `bash launch_lucy.sh` (default launch path). + - Without Git, the installer downloads sub-repositories as ZIP archives. +3. **Python 3** (manual dev workflow only) — [python.org/downloads](https://www.python.org/downloads/). -> If you intend to solely use the control panel visualizer alongside command line tools, you can skip the installation of a third-party Windows X Server. +GUI apps (RViz, Gazebo, rqt) run **natively** on Windows via RoboStack when OpenGL/display support is available. The control panel web viewer does not require a separate X server. ### CPU architecture (x64 / ARM64) -The installer detects the host CPU automatically and builds the matching Docker image — `linux/amd64` on Intel/AMD PCs, `linux/arm64` on Windows-on-ARM devices. Native ARM detection works even though `Lucy.exe` itself is an x64 build running under emulation (it reads the true arch from `PROCESSOR_ARCHITEW6432`). To force a platform, set `LUCY_DOCKER_PLATFORM` (e.g. `linux/amd64`) before running, or drop a `.lucy-docker-platform` file in the install folder. +Pixi resolves **`win-64`** from `pixi.lock` on Intel/AMD and Windows-on-ARM hosts. Colcon uses `--merge-install` on Windows per RoboStack guidance. ## Installation (end users) @@ -40,33 +35,41 @@ The installer: - Creates a **Start Menu** shortcut to `Lucy.exe` - Lets you choose **Fresh install**, **Update**, or **Repair** - Lets you pick a **lucy_ws version** (latest `master` or a release tag) -- **Always runs Install/Update** after setup (opens a console — clones sub-repos on `master`, builds Docker image and workspace, then launches Lucy) +- Runs install/update after setup (clones sub-repos, `pixi install`, colcon build) - Offers **Developer install** (off by default): requires Git, uses SSH clones and `DEV=true` -After setup, open **Lucy** from the Start Menu — it launches the workspace directly. +After setup, open **Lucy** from the Start Menu — it runs `bash launch_lucy.sh` and opens the Control Center. To **update** or **repair**, run **`Lucy-Setup.exe`** again and pick the matching install mode. ### Control Panel -In the **Lucy Control Center**, enable **Core + Control Panel**. Once it is running, the **Lucy Control Panel is accessible in your browser at [http://localhost:4004](http://localhost:4004)** (or the next free port if 4004 is already in use). The launcher also prints the exact URL next to the Control Panel entry once it is up. +In the **Lucy Control Center**, enable **Core + Control Panel**. The panel is at [http://localhost:4004](http://localhost:4004). The launcher prints the exact URL when the panel is running. ## Manual install (developers) -Clone the repo, then run install via CLI (same logic as the installer): +Clone the repo, then install via CLI (same logic as the installer): ```powershell cd C:\Users\\lucy_ws -python windows/Lucy.py --cli install --repos-branch master +python windows\Lucy.py --cli install --repos-branch master ``` -Then launch: +Or use Pixi directly from Git Bash / WSL: + +```bash +./install.sh +pixi run build +pixi run panel-install +``` + +Launch: ```powershell -python windows/Lucy.py +python windows\Lucy.py ``` -Or use the root Linux manager if you prefer WSL/Git Bash: `python3 Lucy.py` (full menu — see the main [README](../README.md)). +Or from Git Bash: `./launch_lucy.sh` or `python3 Lucy.py` (full TUI — see the main [README](../README.md)). ### Advanced CLI (installer internals) @@ -101,5 +104,5 @@ python -c "from PIL import Image; Image.open('path\to\lucy-logo.jpg').save('wind ## Terminal choice -- **Native Windows:** use `Lucy.exe` (installed) or `python windows/Lucy.py` (from a clone). -- **WSL / Git Bash:** use the root `Lucy.py` and `install.sh` / `launch_lucy.sh` instead. +- **Native Windows:** `Lucy.exe` (installed) or `python windows/Lucy.py` (from a clone) — uses Git Bash for launch. +- **Git Bash / WSL:** root `Lucy.py`, `install.sh`, and `launch_lucy.sh` (recommended for developers). diff --git a/windows/install_ops.py b/windows/install_ops.py index 4dbdcf7..ec5996d 100644 --- a/windows/install_ops.py +++ b/windows/install_ops.py @@ -6,7 +6,6 @@ from __future__ import annotations -import hashlib import json import os import platform @@ -23,16 +22,12 @@ REQUIREMENT_DOCS = { "python": ("Python 3", "https://www.python.org/downloads/"), "git": ("Git for Windows", "https://git-scm.com/install/windows"), - "docker": ("Docker Desktop", "https://docs.docker.com/desktop/setup/install/windows-install/"), - "xserver": ("Windows X server (optional)", "https://github.com/marchaesen/vcxsrv/releases"), + "pixi": ("Pixi", "https://pixi.prefix.dev/latest/installation/"), } LUCY_WS_GITHUB = "Sentience-Robotics/lucy_ws" DEFAULT_REPOS_BRANCH = "master" -IMAGE_NAME = "lucy_ros2:jazzy" -WORKSPACE_CONTAINER = "/workspace" -DOCKER_PLATFORM_FILE = ".lucy-docker-platform" -DOCKER_IMAGE_LABEL = "lucy.dockerfile.sha256" +MIN_PIXI_VERSION = "0.78.0" InstallMode = str # "install" | "update" | "repair" | "build-only" @@ -117,12 +112,46 @@ def git_available() -> bool: return False -def docker_available() -> bool: +def pixi_available() -> bool: + return shutil.which("pixi") is not None + + +def _pixi_version_ok() -> bool: + if not pixi_available(): + return False try: - result = _run_quiet(["docker", "version"]) - return result.returncode == 0 + result = _run_quiet(["pixi", "--version"]) except (FileNotFoundError, OSError, subprocess.TimeoutExpired): return False + if result.returncode != 0: + return False + parts = result.stdout.strip().split() + version = parts[1] if len(parts) >= 2 else "" + if not version: + return False + return _version_at_least(version, MIN_PIXI_VERSION) + + +def _version_at_least(version: str, minimum: str) -> bool: + def parse(v: str) -> list[int]: + out: list[int] = [] + for piece in v.split("."): + try: + out.append(int(piece)) + except ValueError: + break + return out + + cur = parse(version) + minv = parse(minimum) + for i in range(max(len(cur), len(minv))): + c = cur[i] if i < len(cur) else 0 + m = minv[i] if i < len(minv) else 0 + if c > m: + return True + if c < m: + return False + return True def python_available() -> bool: @@ -157,13 +186,21 @@ def check_prerequisites( name, url = REQUIREMENT_DOCS["python"] issues.append({"id": "python", "name": name, "url": url, "detail": "python not found"}) - if not docker_available(): - name, url = REQUIREMENT_DOCS["docker"] + if not pixi_available(): + name, url = REQUIREMENT_DOCS["pixi"] + issues.append({ + "id": "pixi", + "name": name, + "url": url, + "detail": "pixi not found in PATH", + }) + elif not _pixi_version_ok(): + name, url = REQUIREMENT_DOCS["pixi"] issues.append({ - "id": "docker", + "id": "pixi", "name": name, "url": url, - "detail": "docker not found or Docker Desktop is not running", + "detail": f"pixi must be >= {MIN_PIXI_VERSION} for multi-platform lock support", }) if developer or not git_available(): @@ -206,12 +243,12 @@ def parse_repos(project_root: str, developer: bool, repos_branch: Optional[str] repos = [] for repo in data.get("repos", []): - name = repo.get("name", "").strip() + name = repo.get("name", "").strip().strip("\r\n") if not name: continue - branch = repos_branch or repo.get("branch", DEFAULT_REPOS_BRANCH) - url_https = (repo.get("url_https") or repo.get("url") or "").strip() - url_ssh = (repo.get("url_ssh") or "").strip() + branch = (repos_branch or repo.get("branch", DEFAULT_REPOS_BRANCH)).strip().strip("\r\n") + url_https = (repo.get("url_https") or repo.get("url") or "").strip().strip("\r\n") + url_ssh = (repo.get("url_ssh") or "").strip().strip("\r\n") url = (url_ssh or url_https) if developer else (url_https or url_ssh) if url: repos.append({"name": name, "branch": branch, "url": url}) @@ -242,18 +279,7 @@ def _safe_rmtree(path: str) -> None: def remove_workspace_src_repo(project_root: str, name: str, run_command: Callable) -> None: - src_path = os.path.join(project_root, "src", name) - _safe_rmtree(src_path) - volume = format_volume_mapping(project_root, WORKSPACE_CONTAINER) - run_command( - [ - "docker", "run", "--rm", - "-v", volume, - IMAGE_NAME, - "-c", f"rm -rf {WORKSPACE_CONTAINER}/src/{name}", - ], - check=False, - ) + _safe_rmtree(os.path.join(project_root, "src", name)) def _extract_zip_to_dest(zip_path: str, dest: str, repo_name: str) -> None: @@ -401,11 +427,6 @@ def install_repos( return effective_method -def format_volume_mapping(host_path: str, container_path: str) -> str: - host_abs = os.path.abspath(host_path) - return host_abs.replace("\\", "/") + ":" + container_path - - def _native_machine() -> str: """Best-effort *native* CPU arch, seeing through Windows x64 emulation. @@ -421,130 +442,43 @@ def _native_machine() -> str: return platform.machine().lower() -def host_container_platform() -> str: - """Map the host CPU to a Docker Linux platform (mirrors docker/ensure_image.sh).""" +def host_pixi_platform() -> str: + """Map the native host to a Pixi platform id (pixi.toml / pixi.lock).""" machine = _native_machine() - if machine in ("x86_64", "amd64", "x86"): - return "linux/amd64" + if sys.platform == "win32": + return "win-64" + if sys.platform == "darwin": + if machine in ("aarch64", "arm64"): + return "osx-arm64" + return "osx-64" if machine in ("aarch64", "arm64"): - return "linux/arm64" - return f"linux/{machine}" - - -def workspace_target_platform(project_root: str) -> str: - """Target platform: LUCY_DOCKER_PLATFORM, then .lucy-docker-platform, then host arch.""" - override = os.environ.get("LUCY_DOCKER_PLATFORM", "").strip() - if override: - return override - marker = os.path.join(project_root, DOCKER_PLATFORM_FILE) - if os.path.isfile(marker): - try: - with open(marker, "r", encoding="utf-8") as f: - value = f.readline().strip() - if value: - return value - except OSError: - pass - return host_container_platform() - - -def _platform_build_settings(target_platform: str) -> tuple[str, int]: - """Return (base_image, install_vnc) for a target platform. - - The Jazzy image is built on ubuntu:24.04 (Noble); arm64 also enables the - optional VNC desktop tooling in Dockerfile.jazzy. - """ - base_image = "ubuntu:24.04" - install_vnc = 1 if target_platform == "linux/arm64" else 0 - if os.environ.get("LUCY_FORCE_VNC", "").strip().lower() in ("1", "true", "yes"): - install_vnc = 1 - return base_image, install_vnc - - -def _dockerfile_build_hash(dockerfile: str) -> str: - """sha256 of the Dockerfile ignoring comments/blank lines (mirrors ensure_image.sh).""" - kept = [] - with open(dockerfile, "r", encoding="utf-8") as f: - for line in f: - line = line.rstrip("\n") - stripped = line.strip() - if not stripped or stripped.startswith("#"): - continue - kept.append(line.rstrip()) - payload = ("\n".join(kept) + "\n").encode("utf-8") - return hashlib.sha256(payload).hexdigest() + return "linux-aarch64" + return "linux-64" -def _current_image_label(image_name: str) -> Optional[str]: - try: - result = subprocess.run( - ["docker", "image", "inspect", image_name, - "--format", '{{index .Config.Labels "' + DOCKER_IMAGE_LABEL + '"}}'], - capture_output=True, text=True, - ) - except FileNotFoundError: - return None - if result.returncode != 0: - return None - return result.stdout.strip() +def host_container_platform() -> str: + """Legacy alias used by Windows CI — returns Pixi platform, not Docker.""" + return host_pixi_platform() -def docker_run_platform_args(project_root: str) -> list[str]: - """`--platform ` so the daemon never guesses the run architecture.""" - return ["--platform", workspace_target_platform(project_root)] +def pixi_install(project_root: str, run_command: Callable, log: Callable[[str], None] = print) -> None: + lock_path = os.path.join(project_root, "pixi.lock") + if not os.path.isfile(lock_path): + log("No pixi.lock — running pixi lock...") + run_command(["pixi", "lock"]) + log("Installing Pixi environment (RoboStack Jazzy)...") + run_command(["pixi", "install"]) -def build_docker_image( +def build_workspace( project_root: str, run_command: Callable, log: Callable[[str], None] = print, - force_rebuild: bool = False, ) -> None: - dockerfile = os.path.join(project_root, "docker", "Dockerfile.jazzy") - target_platform = workspace_target_platform(project_root) - base_image, install_vnc = _platform_build_settings(target_platform) - build_hash = _dockerfile_build_hash(dockerfile) - want_label = f"{build_hash}|{target_platform}|vnc={install_vnc}" - - if not force_rebuild and _current_image_label(IMAGE_NAME) == want_label: - log(f"Docker image {IMAGE_NAME} is up to date ({target_platform}); skipping build.") - return - - log(f"Building Docker image for {target_platform} (base: {base_image})...") - run_command([ - "docker", "build", - "--platform", target_platform, - "-f", dockerfile, - "--build-arg", f"LUCY_FROM_PLATFORM={target_platform}", - "--build-arg", f"LUCY_BASE_IMAGE={base_image}", - "--build-arg", f"LUCY_INSTALL_VNC={install_vnc}", - "--build-arg", f"DOCKERFILE_SHA256={build_hash}", - "--build-arg", f"LUCY_DOCKER_BUILD_PLATFORM={target_platform}", - "-t", IMAGE_NAME, - project_root, - ]) - - -def build_workspace(project_root: str, run_command: Callable, log: Callable[[str], None] = print) -> None: - log("Building workspace inside the container...") - inner_cmd = ( - "source /opt/ros/jazzy/setup.bash && " - "cd /workspace && " - 'rosdep install --from-paths src --ignore-src -r -y --skip-keys="audio_common thais_urdf" && ' - "rm -rf build/camera_ros install/camera_ros && " - "colcon build --symlink-install && " - 'if [ -f src/lucy_control_panel/package.json ]; then ' - "(cd src/lucy_control_panel && yarn install); " - "fi" - ) - volume = format_volume_mapping(project_root, WORKSPACE_CONTAINER) - run_command([ - "docker", "run", "--rm", - *docker_run_platform_args(project_root), - "-v", volume, - IMAGE_NAME, - "-c", inner_cmd, - ]) + log("Building workspace (colcon via Pixi)...") + run_command(["pixi", "run", "build"]) + log("Installing control panel dependencies (yarn)...") + run_command(["pixi", "run", "panel-install"]) def set_dev_mode(project_root: str, enabled: bool) -> None: @@ -600,7 +534,7 @@ def run_install_flow( profile["fetch_method"] = effective set_dev_mode(project_root, dev) - build_docker_image(project_root, run_command, log, force_rebuild=(mode == "repair")) + pixi_install(project_root, run_command, log) if mode in ("install", "update", "repair", "build-only"): build_workspace(project_root, run_command, log) diff --git a/windows/installer/Lucy.nsi b/windows/installer/Lucy.nsi index 519d4cb..7d55b0a 100644 --- a/windows/installer/Lucy.nsi +++ b/windows/installer/Lucy.nsi @@ -1,6 +1,6 @@ ; Lucy Windows installer (NSIS) — bundles the workspace + Lucy.exe, then runs the ; install via "Lucy.exe --cli ...". NSIS is used (instead of Inno Setup) because -; nsExec::ExecToLog streams the long docker/colcon build output live into the +; nsExec::ExecToLog streams the long pixi/colcon build output live into the ; installer's details log. ; ; Build: windows/build_installer.ps1 (requires PyInstaller + NSIS / makensis) @@ -16,8 +16,7 @@ Unicode true !define MyAppURL "https://github.com/Sentience-Robotics/lucy_ws" !define MyAppExeName "Lucy.exe" -!define DOC_DOCKER "https://docs.docker.com/desktop/setup/install/windows-install/" -!define DOC_VCXSRV "https://github.com/marchaesen/vcxsrv/releases" +!define DOC_PIXI "https://pixi.prefix.dev/latest/installation/" !define DOC_GIT "https://git-scm.com/install/windows" !define DOC_PYTHON "https://www.python.org/downloads/" @@ -56,7 +55,7 @@ Var ModeCombo Var VersionCombo Var DevCheck Var RefreshCheck -Var DockerCheck +Var PixiCheck Var PrereqText ; ---------------------------------------------------------------------------- @@ -214,23 +213,23 @@ Function PrereqPageCreate Abort ${EndIf} - StrCpy $PrereqText "Required:$\r$\n Docker Desktop$\r$\n ${DOC_DOCKER}$\r$\n Uncheck $\"Use WSL 2$\" unless you need it for advanced development.$\r$\n Configure Docker resources to use at least 5 GB RAM.$\r$\n$\r$\nOptional (GUI apps):$\r$\n VcXsrv$\r$\n ${DOC_VCXSRV}$\r$\n$\r$\nOptional (developers):$\r$\n Git for Windows$\r$\n ${DOC_GIT}$\r$\n Without Git, repositories are downloaded as ZIP archives.$\r$\n Python 3$\r$\n ${DOC_PYTHON}" + StrCpy $PrereqText "Required:$\r$\n Pixi$\r$\n ${DOC_PIXI}$\r$\n Git for Windows (launch via bash launch_lucy.sh)$\r$\n ${DOC_GIT}$\r$\n$\r$\nOptional (developers):$\r$\n Python 3$\r$\n ${DOC_PYTHON}$\r$\n Without Git, repositories are downloaded as ZIP archives." ; Read-only multiline edit; reliably displays/wraps and scrolls if needed. ${NSD_CreateMLText} 0 0 100% -22u $PrereqText Pop $1 ${NSD_Edit_SetReadOnly} $1 1 - ${NSD_CreateCheckbox} 0 -18u 100% 12u "Docker Desktop is installed" - Pop $DockerCheck + ${NSD_CreateCheckbox} 0 -18u 100% 12u "Pixi is installed" + Pop $PixiCheck nsDialogs::Show FunctionEnd Function PrereqPageLeave - ${NSD_GetState} $DockerCheck $0 + ${NSD_GetState} $PixiCheck $0 ${If} $0 != ${BST_CHECKED} - MessageBox MB_OK|MB_ICONEXCLAMATION "Please confirm that Docker Desktop is installed before continuing." + MessageBox MB_OK|MB_ICONEXCLAMATION "Please confirm that Pixi is installed before continuing." Abort ${EndIf} FunctionEnd @@ -239,13 +238,13 @@ FunctionEnd ; Prerequisite failure dialog (mirrors the old Inno ShowPrerequisitesFailed) ; ---------------------------------------------------------------------------- Function ShowPrerequisitesFailed - StrCpy $0 "Required software is missing or not running, so the workspace install was not started.$\r$\n$\r$\nRequired:$\r$\n Docker Desktop must be installed and running.$\r$\n" + StrCpy $0 "Required software is missing or not available, so the workspace install was not started.$\r$\n$\r$\nRequired:$\r$\n Pixi must be installed and on PATH.$\r$\n" ${If} $DeveloperInstall == "1" StrCpy $0 "$0$\r$\nDeveloper install also requires Git for Windows.$\r$\n" ${EndIf} - StrCpy $0 "$0$\r$\nLucy was copied to your PC. After fixing the items above, run Lucy-Setup.exe again and choose Update.$\r$\n$\r$\nOpen the Docker Desktop install page now?" + StrCpy $0 "$0$\r$\nLucy was copied to your PC. After fixing the items above, run Lucy-Setup.exe again and choose Update.$\r$\n$\r$\nOpen the Pixi install page now?" MessageBox MB_YESNO|MB_ICONEXCLAMATION "$0" IDNO +2 - ExecShell "open" "${DOC_DOCKER}" + ExecShell "open" "${DOC_PIXI}" FunctionEnd ; ---------------------------------------------------------------------------- @@ -254,7 +253,8 @@ FunctionEnd Section "Install" SetOutPath "$INSTDIR" File "..\..\dist\${MyAppExeName}" - File "..\..\docker\Dockerfile.jazzy" + File "..\..\pixi.toml" + File "..\..\pixi.lock" File "..\..\install.sh" File "..\..\launch_lucy.sh" File "..\..\Lucy.py" @@ -265,9 +265,6 @@ Section "Install" SetOutPath "$INSTDIR\config" File /r "..\..\config\*" - SetOutPath "$INSTDIR\docker" - File /r "..\..\docker\*" - SetOutPath "$INSTDIR\windows" File /r /x "installer" /x "build_installer.ps1" "..\..\windows\*" @@ -313,7 +310,7 @@ Section "Install" StrCpy $2 "$2 --refresh-workspace" ${EndIf} - DetailPrint "Running $InstallMode (cloning repos + building Docker image + workspace; this can take a while)..." + DetailPrint "Running $InstallMode (cloning repos + pixi install + colcon build; this can take a while)..." nsExec::ExecToLog $2 Pop $3 ${If} $3 == 0