Skip to content

Exercise 1: Your first ROS 2 workspace

30 minutes, hands-on

Work in: exercises/01-ros-workspace/ · Solution: solutions/01-ros-workspace/ · After: Pixi in 30 minutes

Goal: a pixi.toml and pixi.lock that give you a running ROS 2 turtlesim, a custom node of your own, the same workspace targeting two ROS distributions, and a PyTorch node that resolves for a GPU box and a Jetson from your own laptop.

This page is a series of small exercises. Each one lists what to do, and the commands are folded away in a solution underneath. Try it on your own first, the cheat sheet helps with the command names, and open the solution when you are stuck.

Work inside the workshop repository

The exercises build on packages that ship in this repository, so start from the right directory:

cd exercises/01-ros-workspace

There is a src/turtle_dancer/ package in there already, but no pixi.toml yet. Creating it is the first exercise.

1.1 Initialize a workspace

Your turn

  1. Create a Pixi manifest in this directory, with the Pixi CLI.
  2. Open pixi.toml and check which platforms and channels a new workspace starts with.
Solution
# 1
pixi init
# 2: open pixi.toml in your editor

Resulting pixi.toml file (the author and platform are read from your machine):

exercises/01-ros-workspace/pixi.toml
[workspace]
authors = ["Jane Doe <jane.doe@example.com>"]
channels = ["conda-forge"]
name = "01-ros-workspace"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]

[dependencies]

1.2 Install ROS 2

ROS packages live on a RoboStack channel, not on conda-forge. A channel works like an apt source, except it is written down in your manifest.

Your turn

  1. Add the https://prefix.dev/robostack-lyrical channel, at a higher priority than conda-forge. Hint: pixi workspace channel add --help.
  2. Add ros-lyrical-ros-base and ros-lyrical-turtlesim as dependencies.
  3. Check what actually landed in the environment.
  4. Prove ROS runs by listing its topics. Hint: any command runs inside the environment with pixi run <command>, or open a shell inside it with pixi shell.
Solution
# 1
pixi workspace channel add --prepend https://prefix.dev/robostack-lyrical
# 2
pixi add ros-lyrical-ros-base ros-lyrical-turtlesim
# 3
pixi list
# 4
pixi run ros2 topic list
# or from a shell inside the environment:
pixi shell
ros2 topic list

A pixi.lock appeared next to the manifest. It records every package that step 2 resolved, and pixi run installs from it before running anything.

Resulting pixi.toml file:

exercises/01-ros-workspace/pixi.toml
[workspace]
authors = ["Jane Doe <jane.doe@example.com>"]
channels = ["https://prefix.dev/robostack-lyrical", "conda-forge"]
name = "01-ros-workspace"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]

[dependencies]
ros-lyrical-ros-base = ">=0.13.0,<0.14"
ros-lyrical-turtlesim = ">=1.10.9,<2"

1.3 Turn the commands into tasks

Commands you reuse get a name in the manifest, so your teammates run them without knowing the full incantation.

Your turn

  1. Add a task sim that runs ros2 run turtlesim turtlesim_node.
  2. Add a task teleop for ros2 run turtlesim turtle_teleop_key, and a task topics for ros2 topic list.
  3. Open two terminals and drive the turtle: the simulator in one, the teleop in the other.
Solution
# 1
pixi task add sim "ros2 run turtlesim turtlesim_node"
# 2
pixi task add teleop "ros2 run turtlesim turtle_teleop_key"
pixi task add topics "ros2 topic list"
# 3: in two terminals
pixi run sim        # a turtle appears
pixi run teleop     # click this terminal, use the arrow keys

Resulting [tasks] table:

exercises/01-ros-workspace/pixi.toml
[tasks]
sim = "ros2 run turtlesim turtlesim_node"
teleop = "ros2 run turtlesim turtle_teleop_key"
topics = "ros2 topic list"

1.4 Build your own node with colcon

src/turtle_dancer/ is a small ROS 2 C++ package, written for you already. Building it needs a toolchain and the ROS libraries it includes, and those come from the same channels as everything else. On Windows, the compiler and SDK must already be installed: see Before you start.

Your turn

  1. Add ros-dev-tools: one package that brings colcon, CMake and compiler tooling. The ROS libraries the node uses are already there, ros-lyrical-ros-base includes them.
  2. Build the workspace with colcon.
  3. Try to run your node with ros2 run turtle_dancer dance and read the error.
  4. Fix it: make Pixi source the colcon overlay in install/ whenever the environment activates. Hint: look up activation in the Pixi documentation, and mind that the overlay's filename differs per platform.
  5. Add a build task, and a dance task that depends on it.
  6. Run the simulator in one terminal and your node in another.
Solution
# 1
pixi add ros-dev-tools
# 2
pixi run colcon build
# 3
pixi run ros2 run turtle_dancer dance
# Package 'turtle_dancer' not found

That error is expected. colcon built the node into install/, and ros2 only finds it once that overlay is sourced. You can source it by hand in every terminal, or let Pixi do it with an activation script that runs whenever the environment activates.

4: the overlay has a different filename on Windows, so pick your platform:

pixi workspace activation scripts add --target unix install/setup.sh
pixi workspace activation scripts add --target win-64 install/setup.bat

The script only exists after the first colcon build, so Pixi warns until then.

# 5
pixi task add build "colcon build"
pixi task add dance "ros2 run turtle_dancer dance" --depends-on build
# 6: in two terminals
pixi run sim
pixi run dance    # the turtle starts dancing

Resulting pixi.toml file:

exercises/01-ros-workspace/pixi.toml
[workspace]
authors = ["Jane Doe <jane.doe@example.com>"]
channels = ["https://prefix.dev/robostack-lyrical", "conda-forge"]
name = "01-ros-workspace"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]
sim = "ros2 run turtlesim turtlesim_node"
teleop = "ros2 run turtlesim turtle_teleop_key"
topics = "ros2 topic list"
build = "colcon build"
dance = { cmd = "ros2 run turtle_dancer dance", depends-on = ["build"] }

[dependencies]
ros-lyrical-ros-base = ">=0.13.0,<0.14"
ros-lyrical-turtlesim = ">=1.10.9,<2"
ros-dev-tools = ">=1.0.2,<2"

[target.unix.activation]
scripts = ["install/setup.sh"]

colcon left build/, install/ and log/ directories behind, exactly as it does outside Pixi.

1.5 Two ROS distros, one workspace

A second ROS distribution is a second environment in the same manifest. The toolchain and run tasks are shared, but compiled packages and Python modules belong to the distro that built them. Give each environment its own colcon directories and activate only its matching overlay.

Your turn

  1. Move the Lyrical channel and ros-lyrical packages into [environments.default].
  2. Put activation under the default environment instead, pointing at install/default/local_setup.sh.
  3. Add a kilted environment with its own channel, packages and activation scripts under install/kilted/.
  4. Replace the build task so colcon uses build/<environment>, install/<environment> and log/<environment>. Pixi sets $PIXI_ENVIRONMENT_NAME in its cross-platform task shell.
  5. From a normal terminal outside pixi shell, build once in each environment. Exit any previously activated shell before switching distros.
  6. Run turtlesim and dance task from each distribution. Stop each simulator before starting the next one.
Solution

1: keep only conda-forge in the workspace channel list. Move the ROS dependencies and activation into the default environment, and remove the old workspace-level activation tables:

exercises/01-ros-workspace/pixi.toml
[environments.default]
channels = ["https://prefix.dev/robostack-lyrical", "conda-forge"]

[environments.default.dependencies]
ros-lyrical-ros-base = ">=0.13"
ros-lyrical-turtlesim = ">=1.10"

[environments.default.target.unix.activation]
scripts = ["install/default/setup.sh"]

[environments.default.target.win-64.activation]
scripts = ["install/default/setup.bat"]

2: Kilted has a different channel, package prefix and overlay directory:

exercises/01-ros-workspace/pixi.toml
[environments.kilted]
channels = ["https://prefix.dev/robostack-kilted", "conda-forge"]

[environments.kilted.dependencies]
ros-kilted-ros-base = "*"
ros-kilted-turtlesim = "*"

[environments.kilted.target.unix.activation]
scripts = ["install/kilted/setup.sh"]

[environments.kilted.target.win-64.activation]
scripts = ["install/kilted/setup.bat"]

Use local_setup rather than setup: Pixi already supplies the selected ROS underlay. Colcon's setup script also replays the underlays recorded during the build, which can reintroduce another distro. Separate directories keep the CMake cache, executables and installed Python modules apart too.

3: replace the old build = "colcon build" task with:

exercises/01-ros-workspace/pixi.toml
[tasks.build]
cmd = "colcon --log-base log/$PIXI_ENVIRONMENT_NAME build --base-paths src --build-base build/$PIXI_ENVIRONMENT_NAME --install-base install/$PIXI_ENVIRONMENT_NAME"
description = "Build the packages into this ROS environment's own colcon overlay"

--log-base is a colcon option before the build subcommand. --base-paths src restricts package discovery to the source tree. The old flat install/setup.* files from 1.4 are no longer activated.

# 4: build both overlays before launching nodes
pixi run -e default build
pixi run -e kilted build
# 5: stop each simulator with Ctrl+C before the next command
pixi run -e default sim
pixi run -e kilted sim
pixi run -e default sim

Everything at workspace level belongs to the default feature, and every environment includes it. The sim, dance and build tasks stay shared; the build paths and activation now select the correct distro.

1.6 Give the turtle a PyTorch brain

src/turtle_brain/ is a pre-written ament_python ROS package. It drives the turtle with a small PyTorch computation, on the GPU when one is available and the CPU otherwise.

Your turn

  1. Add pytorch as a dependency.
  2. Add a brain task that runs ros2 run turtle_brain brain and depends on build.
  3. Run pixi run build once to install the package before activating its overlay in the next command.
  4. Run the simulator and the brain, in two terminals.
Solution
# 1
pixi add pytorch
# 2
pixi task add brain "ros2 run turtle_brain brain" --depends-on build
# 3: build before the next command activates the overlay
pixi run build
# 4: in two terminals
pixi run sim
pixi run brain    # the turtle moves, and the node logs "thinking on: cpu"

After the build, pixi run ros2 pkg executables turtle_brain should list turtle_brain brain. You can also launch it directly with pixi run ros2 run turtle_brain brain, or with ros2 run turtle_brain brain inside pixi shell. On a fresh checkout, build once in each environment before launching nodes there. Start a new pixi run or pixi shell after that build so its matching overlay can be activated.

On a laptop the node runs on the CPU. Next you give it a GPU to think on.

1.7 Target a GPU, and every platform

A GPU build of PyTorch only resolves for a platform that has CUDA. You tell Pixi a platform has a GPU by giving it a CUDA version, the __cuda virtual package from CUDA.

Your turn

  1. Add a CUDA platform: name it cuda-linux-64, on linux-64, with CUDA 12. Give it priority over ordinary Linux so a compatible GPU host selects the CUDA build.
  2. Add the ordinary platforms too: linux-64, osx-arm64, win-64.
  3. Make PyTorch use the GPU build where CUDA is present, and the CPU build everywhere else. Hint: a when condition on the dependency.
  4. Inspect what each platform would get, all from your own machine.
Solution
# 1
pixi workspace platform add cuda-linux-64=linux-64 --cuda 12
# 2
pixi workspace platform add linux-64 osx-arm64 win-64
pixi workspace platform move cuda-linux-64 --to-top

3: the GPU build cannot install without CUDA, so make it conditional and keep a CPU fallback. Edit [dependencies]:

exercises/01-ros-workspace/pixi.toml
[dependencies]
# GPU build where CUDA is present, CPU build everywhere else.
pytorch-gpu = { version = ">=2.5", when = "__cuda" }
pytorch = ">=2.5"
# 4
pixi list --platform win-64          # your colleague on Windows: CPU build
pixi list --platform cuda-linux-64   # the GPU box: CUDA build

Solving is not installing, so this works from any laptop in the room, even the Macs.

1.8 Target a Jetson

A Jetson is linux-aarch64 with CUDA. That is just another platform, so you add it the same way, with its own CUDA version.

Your turn

  1. Add a jetson platform on linux-aarch64 with CUDA 13.
  2. Solve the robot's environment from your laptop.
Solution
# 1
pixi workspace platform add jetson=linux-aarch64 --cuda 13
# 2
pixi list --platform jetson

We assume JetPack 7.2 or newer

JetPack 7.2 and up ship CUDA 13, so that is what we target here. On an older JetPack the CUDA version is different (JetPack 6 ships CUDA 12.6), so match cuda to what your robot actually runs.

That is a complete CUDA environment for a machine you are not sitting at. You solve on your laptop and install on the Jetson.

1.9 Run it on a real GPU

This step needs an NVIDIA GPU

Declaring CUDA lets Pixi solve an environment; it does not give your laptop a GPU. Running this step needs a Linux machine with an NVIDIA GPU and a compatible NVIDIA driver, or the Jetson from 1.8. An Apple GPU cannot run CUDA. If you don't have access to suitable hardware, follow the Brev setup with the instructors, then run the commands below inside the remote terminal. Wait for the instructor coupon before creating an instance.

The supplied src/turtle_brain/turtle_brain/check_cuda.py runs a short tensor computation directly on CUDA. It needs neither ROS nodes nor a turtlesim window, so you can run it over SSH. It fails if CUDA is unavailable instead of silently using the CPU.

Your turn

  1. On the GPU machine, check that nvidia-smi can see the device.
  2. Add a cuda-check task that runs python src/turtle_brain/turtle_brain/check_cuda.py.
  3. Run it on the cuda-linux-64 platform and inspect the device name and computed result. On the Jetson, select jetson instead.
  4. Run the brain on the same platform. Stop it with Ctrl+C after it logs thinking on: cuda.
Solution

From your exercise directory on the GPU machine:

nvidia-smi
pixi task add cuda-check "python src/turtle_brain/turtle_brain/check_cuda.py"
pixi run --platform cuda-linux-64 cuda-check

The check prints the GPU name, compute capability, supported architectures and GPU result: 8.0. Reading the result waits for the CUDA computation to finish, so detecting a driver alone cannot pass this check. If it fails, check the selected platform and ask an instructor to check the driver and PyTorch build.

pixi run --platform cuda-linux-64 build
pixi run --platform cuda-linux-64 brain

Without a GPU

You can finish the CPU brain and inspect the GPU/Jetson package selections from your laptop. To try the CUDA computation during the workshop, join the Brev setup or follow along on an instructor's GPU machine. A CPU run or successful solve is not a successful CUDA check.

The full pixi.toml
solutions/01-ros-workspace/pixi.toml
# Solution to Exercise 1: Your first ROS 2 workspace.
#
# A complete ROS 2 environment with no ROS installed on the machine, and the
# same workspace targeting two ROS distributions at once:
#
#     pixi run sim              # turtlesim on Lyrical
#     pixi run teleop           # drive it with the arrow keys
#     pixi run -e kilted sim    # the same workspace, on Kilted
#     pixi run brain            # PyTorch on the CPU, or CUDA when available
#     pixi run --platform cuda-linux-64 cuda-check  # requires an NVIDIA GPU
#
# Colcon builds each distro into its own build/, install/ and log/ subdirectories.
# Run `pixi run -e <environment> build` once before launching nodes there.

[workspace]
name = "turtle-workspace"
description = "A ROS 2 workspace managed entirely by Pixi, targeting two distros"
channels = ["conda-forge"]
platforms = [
  { name = "cuda-linux-64", platform = "linux-64", cuda = "12" },
  "linux-64",
  "osx-arm64",
  "win-64",
  { name = "jetson", platform = "linux-aarch64", cuda = "13" },
]
version = "0.1.0"

# ---------------------------------------------------------------------------
# Shared by every environment: the toolchain and the tasks.
# ---------------------------------------------------------------------------
[dependencies]
# Colcon, CMake and compiler tooling for building our own package.
# Windows needs Visual Studio C++ Build Tools and a Windows SDK installed first;
# see docs/setup.md for Microsoft's installation instructions.
ros-dev-tools = ">=1.0"
pytorch = ">=2.5"
pytorch-gpu = { version = ">=2.5", when = "__cuda" }

[tasks.sim]
cmd = "ros2 run turtlesim turtlesim_node"
description = "Start the turtlesim window"

[tasks.teleop]
cmd = "ros2 run turtlesim turtle_teleop_key"
description = "Drive the turtle with the arrow keys"

[tasks.topics]
cmd = "ros2 topic list"
description = "List the ROS topics: proof that ROS works without sourcing anything"

[tasks.build]
cmd = "colcon --log-base log/$PIXI_ENVIRONMENT_NAME build --base-paths src --build-base build/$PIXI_ENVIRONMENT_NAME --install-base install/$PIXI_ENVIRONMENT_NAME"
description = "Build the packages into this ROS environment's own colcon overlay"

[tasks.test]
cmd = "python -c \"import os; print('ros:', os.environ['ROS_DISTRO'], 'ok')\""
description = "Prove ROS is active: what CI runs"

[tasks.test-overlay]
cmd = "python check_overlay.py"
description = "Check that ROS and both local packages belong to the selected environment"

# Run `pixi run build` once before the first launch so activation can find the overlay.
[tasks.brain]
cmd = "ros2 run turtle_brain brain"
depends-on = ["build"]
description = "Run the PyTorch ROS executable (build once before the first launch)"

[tasks.cuda-check]
cmd = "python src/turtle_brain/turtle_brain/check_cuda.py"
description = "Run a CUDA tensor computation; fail if CUDA is unavailable"

# Each environment activates only its own colcon overlay, without replaying the
# underlay chain recorded by setup.sh. Build once before launching either node.
[tasks.dance]
cmd = "ros2 run turtle_dancer dance"
depends-on = ["build"]
description = "Run our own node (the colcon overlay is sourced on activation)"


# ---------------------------------------------------------------------------
# ROS 2 Lyrical: the main workshop distro. The default
# environment, declared inline: its own channel and its own packages, on top
# of everything shared above. The rclcpp and geometry-msgs libraries the node
# uses come with ros-base.
# ---------------------------------------------------------------------------
[environments.default]
channels = ["https://prefix.dev/robostack-lyrical", "conda-forge"]

[environments.default.dependencies]
ros-lyrical-ros-base = ">=0.13"
ros-lyrical-turtlesim = ">=1.10"

[environments.default.target.unix.activation]
scripts = ["install/default/setup.sh"]

[environments.default.target.win-64.activation]
scripts = ["install/default/setup.bat"]

# ---------------------------------------------------------------------------
# ROS 2 Kilted: a different distribution, same workspace. A different channel
# and a different package prefix; the tasks come from the shared set above.
# ---------------------------------------------------------------------------
[environments.kilted]
channels = ["https://prefix.dev/robostack-kilted", "conda-forge"]

[environments.kilted.dependencies]
ros-kilted-ros-base = "*"
ros-kilted-turtlesim = "*"

[environments.kilted.target.unix.activation]
scripts = ["install/kilted/setup.sh"]

[environments.kilted.target.win-64.activation]
scripts = ["install/kilted/setup.bat"]

Check your work

pixi run topics           # ROS is alive
pixi run dance            # your node drives the turtle
pixi run brain            # a PyTorch node drives the turtle
pixi run -e kilted test   # ...and the same workspace runs a different distro
pixi list --platform jetson   # a Jetson environment, resolved from your laptop

You should see a list of /turtle1/... topics, a dancing turtle, the brain logging its device, ros: kilted ok, and a full aarch64 environment for the Jetson.

Going further

Finished early? Try these.

  • Add a viewer with pixi add ros-lyrical-rviz2, then pixi run rviz2.
  • Delete the .pixi/ folder, run pixi install, and watch the environment rebuild from the lockfile.

With your own workspace

Doing this on your own ROS project is the same steps. Run pixi init in the repo, add the robostack-<your-distro> channel, and add the ros-<your-distro>-* packages you depend on. Then get colcon build running inside the Pixi environment, like exercise 1.4.


Next: Exercise 2: Build ROS packages with Pixi.