Build and Installation

Repositories

Requirements

  • clang or gcc
  • make or ninja
  • scdoc (source) (for manpages)
  • pandoc (for web docs)

Linux (e.g. Debian)

install scdoc (for docs) and build deps:

sudo apt install scdoc git build-essential llvm clang clang-tidy clang-format

1. Install mirage:

git clone https://git.sr.ht/~alecgraves/mirage
cd mirage
make
make check
make docs

Install (as root):

# make install install-docs

Install mirage Python package:

make python

Go back to source directory:

cd ..

2. Install quicksand:

git clone https://git.sr.ht/~alecgraves/quicksand
cd quicksand
make
make check
make docs

Install (as root):

# make install install-docs

Install quicksand Python package:

make python

Go back to source directory:

cd ..

3. Install ros3 🌹:

git clone https://git.sr.ht/~alecgraves/ros3
cd ros3
make
make check
make docs

Install (as root):

# make install install-docs

Install ros3 Python package:

make python
make check-python

Webdocs (this guide):

make docs

Congrats, you have installed ros3!

macOS

Use the Darwin makefile for ros3 and quicksand. mirage currently uses its regular Makefile.

Install Xcode Command Line Tools first:

xcode-select --install

Optional tools:

  • scdoc is needed for make docs
  • pandoc is needed for make webdocs
  • the full make -f Makefile.darwin check target also expects working clang-tidy and gcc executables on PATH

Example with Homebrew:

brew install scdoc pandoc llvm gcc

If your package manager installs versioned binary names instead of plain clang-tidy or gcc, set CLANG_TIDY=... and GCC_ANALYZER=... when running the full check targets.

Pick one install prefix and use it consistently for mirage, quicksand, and ros3.

  • Apple Silicon Homebrew: PREFIX=/opt/homebrew
  • Intel Homebrew or a manual /usr/local install: PREFIX=/usr/local

Example:

export PREFIX=/opt/homebrew

1. Install mirage

git clone https://git.sr.ht/~alecgraves/mirage
cd mirage
make PREFIX="$PREFIX"
make PREFIX="$PREFIX" check-runtime
sudo make PREFIX="$PREFIX" install
make PREFIX="$PREFIX" docs
sudo make PREFIX="$PREFIX" install-docs
cd ..

2. Install quicksand

git clone https://git.sr.ht/~alecgraves/quicksand
cd quicksand
make -f Makefile.darwin PREFIX="$PREFIX"
make -f Makefile.darwin PREFIX="$PREFIX" check
sudo make -f Makefile.darwin PREFIX="$PREFIX" install
make -f Makefile.darwin PREFIX="$PREFIX" docs
sudo make -f Makefile.darwin PREFIX="$PREFIX" install-docs
cd ..

3. Install ros3

git clone https://git.sr.ht/~alecgraves/ros3
cd ros3
make -f Makefile.darwin PREFIX="$PREFIX"
make -f Makefile.darwin PREFIX="$PREFIX" check-runtime
sudo make -f Makefile.darwin PREFIX="$PREFIX" install
make -f Makefile.darwin PREFIX="$PREFIX" docs
sudo make -f Makefile.darwin PREFIX="$PREFIX" install-docs

Use the full analysis + sanitizer suite only if clang-tidy and gcc are configured and the dependency repos are also present as siblings at ../mirage and ../quicksand (or passed via MIRAGE_DIR=... QUICKSAND_DIR=...):

make -f Makefile.darwin PREFIX="$PREFIX" check

mirage also has a heavier make check target, but it similarly expects extra analysis tooling on PATH.

Python bindings:

make -f Makefile.darwin PREFIX="$PREFIX" python
make -f Makefile.darwin PREFIX="$PREFIX" check-python

Current limitation: lang/python/setup.py still hardcodes /usr/local/include for dependency headers. If you install dependencies somewhere else, the Python build may fail until that setup script is made prefix-aware.

Windows

Use wsl for now.

(TODO Alec, 2026): add native windows support

Build web docs

make webdocs
# or
ninja webdocs

Project Setup

Minimal layout

my_ros3_app/
  src/
    pub.c
    sub.c
  Makefile

Compile against ros3

Have your makefile essentially execute the following:

cc -O2 -I/usr/local/include src/pub.c -L/usr/local/lib -lros3 -lmirage -lquicksand -o pub
cc -O2 -I/usr/local/include src/sub.c -L/usr/local/lib -lros3 -lmirage -lquicksand -o sub

Expose the following options for install in your build file:

PREFIX ?= /usr/local
BIN_INSTALL_DIR := $(DESTDIR)$(PREFIX)/bin
LIB_INSTALL_DIR := $(DESTDIR)$(PREFIX)/lib
INCLUDE_INSTALL_DIR := $(DESTDIR)$(PREFIX)/include

Then, install with Makefile targets similar to the following:

install-bin: $(BIN_TARGETS)
  mkdir -p $(BIN_INSTALL_DIR)
  install -m 0755 $(BIN_TARGETS) $(BIN_INSTALL_DIR)/

install-lib: $(LIB_TARGETS) $(PC_FILE)
  mkdir -p $(LIB_INSTALL_DIR) $(INCLUDE_INSTALL_DIR)
  install -m 0644 $(BUILD_DIR)/libros3.dylib $(BUILD_DIR)/libros3.a $(BUILD_DIR)/libros3_gcc_lto.a $(LIB_INSTALL_DIR)/
  install -m 0644 $(HEADER) $(INCLUDE_INSTALL_DIR)/ros3.h

These will be set to a ros3 workspace (ros3/bin ros3/lib ros3/include) during development.

Runtime namespace

export ROSE_BASE=robot1

Startup workflow

Terminal 1 (master):

./build/rosecore

Terminal 2 (subscriber):

ROSE_BASE=robot1 ./sub

Terminal 3 (publisher):

ROSE_BASE=robot1 ./pub

Use one namespace per robot or environment to avoid topic collisions.

Pub/Sub in C

Publisher

#include <ros3.h>
#include <mirage.h>

int main(int argc, char **argv) {
    rose_node *n = rose_init((char)argc, argv, "c_pub", NULL, NULL);
    rose_publisher *p = rose_create_pub(n, "/chatter", -1, 1024, 128);
    mirage_msg *m = mirage_create(1024, NULL);
    while (rose_ok(n)) {
        mirage_write_start(m);
        mirage_write_string(m, "hello from c", -1);
        rose_publish(p, m);
        rose_sleep(0.1);
    }
    return 0;
}

Subscriber

#include <ros3.h>
#include <mirage.h>
#include <stdio.h>

int main(int argc, char **argv) {
    rose_node *n = rose_init((char)argc, argv, "c_sub", NULL, NULL);
    rose_subscriber *s = rose_create_sub(n, "/chatter", -1, 1, NULL);
    mirage_msg *m = mirage_create(1024, NULL);
    char text[256]; i64 len = 0;
    while (rose_ok(n)) {
        if (rose_read(s, m) >= 0) {
            mirage_read_start(m);
            mirage_read_string(m, text, &len, sizeof(text));
            printf("%.*s\n", (int)len, text);
        }
        rose_sleep(0.01);
    }
    return 0;
}

Pub/Sub in C++

Example flow

./build/rosecore &
./build/test_cpp_subscriber cpp_chatter 10
./build/test_cpp_publisher cpp_chatter 10 100

C API from C++

ros3.h is C-compatible in C++ mode. Use the same pub/sub API from C++ translation units.

Typical compile line

c++ -O2 -std=c++17 -I/usr/local/include app.cpp -L/usr/local/lib -lros3 -lmirage -lquicksand -o app

Pub/Sub in Python

Install bindings

cd lang/python
pip install -e .

Publisher

import ros3 as rose

node = rose.Node("py_publisher")
publisher = node.publisher("chatter", message_size=1024, rate=128)

while node.ok():
    publisher.publish(["hello from python"])
    rose.sleep(0.1)

Subscriber

import ros3 as rose

node = rose.Node("py_subscriber")
subscriber = node.subscriber("chatter")

while node.ok():
    for message in subscriber:
        print(f"Received: {message}")
    rose.sleep(0.01)

publisher.publish(...) encodes with Mirage automatically, and subscriber yields decoded Python objects.

ROS3 Tools

Overview

  • rosecore: node/topic registry and query endpoint
  • rosenode: inspect/kill nodes
  • rosetopic: list/inspect/echo/measure/remove topics
  • rosenet_tx: bridge local topic to TCP/UDP
  • rosenet_rx: bridge TCP/UDP to local topic
  • roseserial: serial device bridge for microcontrollers

rosecore

Topic: master registry and discovery service.

Commands and options:

  • rosecore
  • No CLI flags.

rosenode

Topic: node lifecycle and node metadata.

Commands and options:

  • rosenode list
  • rosenode info <name>
  • rosenode info --pid <pid>
  • rosenode kill <name>
  • rosenode kill -9 <name>

rosetopic

Topic: topic discovery, inspection, and message streaming.

Commands and options:

  • rosetopic list
  • rosetopic info <topic>
  • rosetopic echo <topic>
  • rosetopic hz <topic>
  • rosetopic rm <topic> - delete an old topic (e.g. if you changed the size)

rosenet_tx

Topic: outbound network bridge from ROS3 topic to TCP/UDP.

Commands and options:

  • Positional form: rosenet_tx <topic> <host> <port> [--udp]
  • Flag form: rosenet_tx --topic <topic> --host <host> --port <port> [--udp] [--message_size N] [--read_last 0|1]
  • Options: --topic, --host, --port, --udp, --message_size, --read_last

rosenet_rx

Topic: inbound network bridge from TCP/UDP to ROS3 topic.

Commands and options:

  • Positional form: rosenet_rx <topic> <port> <msg_size> <message_rate> [--udp]
  • Flag form: rosenet_rx --topic <topic> --port <port> [--udp] [--message_size N] [--message_rate N]
  • Options: --topic, --port, --udp, --message_size, --message_rate

roseserial

Topic: serial microcontroller bridge to ROS3 topics. If the TX topic is missing at startup, roseserial keeps running and retries the subscriber connection once per second.

Commands and options:

  • roseserial <device> <rx_topic> <tx_topic> <baud> <msg_size> <slots>
  • No optional flags.

Common workflow

# list nodes and topics
rosenode list
rosetopic list

# inspect one node/topic
rosenode info demo
rosetopic info /camera

# stream payload bytes from a topic
rosetopic echo /camera

# measure topic rate
rosetopic hz /camera

# unlink a topic shared-memory segment
rosetopic rm /camera

Networking examples

rosenet_tx --topic /cam --host 10.0.0.2 --port 19999 --message_size 65536 --read_last 1
rosenet_rx --topic /cam_remote --port 19999 --message_size 65536 --message_rate 64

Shared environment

  • ROSE_BASE: namespace prefix (default rose)
  • ROSE_QUERY_TIMEOUT_MS: query timeout in milliseconds

roseserial

roseserial bridges serial frames and ROS3 topics.

Start bridge

roseserial /dev/ttyUSB0 /mcu/rx /mcu/tx 115200 1024 256

If tx_topic does not exist yet, roseserial keeps running and retries that subscription once per second. The serial -> rx_topic path continues working immediately.

Frame format

  • 2-byte little-endian payload length
  • payload bytes (Mirage-encoded message)

Arduino sender using Mirage embedded API

#include <Arduino.h>
#include <mirage_embedded.h>

static uint8_t frame[128];
static mirage_msg msg;
static int16_t count = 0;

void setup() {
    Serial.begin(115200);
    mirage_init(&msg, frame, (int16_t)sizeof(frame));
}

void loop() {
    // Canonical Mirage/WXF encoding: List["Hello world", count]
    int16_t ret = 0;
    ret |= mirage_write_start(&msg);
    ret |= mirage_write_fn(&msg, "List", (int16_t)(sizeof("List") - 1), 2);
    ret |= mirage_write_string(&msg, "Hello world", (int16_t)(sizeof("Hello world") - 1));
    ret |= mirage_write_i16(&msg, count);
    if (ret < 0) return;

    const uint16_t len = (uint16_t)msg.length;
    Serial.write((uint8_t)(len & 0xFF));
    Serial.write((uint8_t)((len >> 8) & 0xFF));
    Serial.write(msg.data, len);
    count = (count + 1) % 4096;

    delay(1000);
}

Use mirage_embedded.h on Arduino so payloads match ROS3 Mirage decoding.

Multi-machine Usage

Pattern

  • Run one rosecore per machine namespace.
  • Use rosenet_tx and rosenet_rx to bridge topics over TCP.

Example

Machine A:

export ROSE_BASE=robot_a
./build/rosenet_tx --topic /cam --host 10.0.0.2 --port 19999

Machine B:

export ROSE_BASE=robot_b
./build/rosenet_rx --topic /cam_remote --port 19999 --message_size 65536 --message_rate 64

Notes

  • Keep message sizes conservative for latency stability.
  • Use dedicated bridge topics for high-rate streams.
  • Keep clocks reasonably synchronized when debugging timestamps.
  • Use SSH tunneling or a VPN when bridging across untrusted networks.

Mirage Datatypes Used by ROS3

ROS3 payloads are Mirage-encoded message buffers. These are the common types you will see.

Core type map

Type Write API Read API Typical ROS3 usage
i8/i16/i32/i64 mirage_write_i* mirage_read_i* counters, ids, timestamps
f64 mirage_write_f64 mirage_read_f64 pose, velocity, sensor scalars
string mirage_write_string mirage_read_string text topics, names, status
symbol mirage_write_sym mirage_read_sym enum-like labels
binstring mirage_write_bin mirage_read_bin compressed payloads, frames
function mirage_write_fn mirage_read_fn typed message envelope
List function mirage_write_fn(..., "List", ..., n) + element writes mirage_read_fn + element reads ordered lists of values
numericarray mirage_write_numericarray mirage_read_numericarray dense numeric arrays
association/rule mirage_write_association + mirage_write_assoc_rule mirage_read_association + mirage_read_assoc_rule key/value parameter blocks

Common writer calls (publisher encode example)

mirage_write_start(m);
mirage_write_fn(m, "List", -1, 2);

// store a pose in a sub-list
mirage_write_fn(m, "List", -1, 3);
mirage_write_f64(m, x);
mirage_write_f64(m, y);
mirage_write_f64(m, theta);

mirage_write_i64(m, (i64) timestamp_ns);

Common reader calls (subscriber decode example)

char name[8];
i64 nlen = 0, argc = 0;
char pose_name[8];
i64 pose_nlen = 0, pose_argc = 0;
f64 x = 0.0, y = 0.0, theta = 0.0;
i64 timestamp_i64 = 0;
u64 timestamp_ns = 0;

mirage_read_start(m);
mirage_read_fn(m, name, &nlen, sizeof(name), &argc);
name[nlen] = '\0';          // "List"

mirage_read_fn(m, pose_name, &pose_nlen, sizeof(pose_name), &pose_argc);
pose_name[pose_nlen] = '\0'; // "List"

mirage_read_f64(m, &x);
mirage_read_f64(m, &y);
mirage_read_f64(m, &theta);

mirage_read_i64(m, &timestamp_i64);
timestamp_ns = (u64) timestamp_i64;

Datatype guidance

  • Use string for human-readable text.
  • Store ordered lists of values in a Mirage List function rather than inventing ad hoc repeated fields. The Python decoder treats a List function as a native Python list; see ../mirage/lang/python/mirage/mirage.py for reference.
  • Use binstring for compressed frames or custom binary payloads.
  • Use numericarray for large numeric tensors and images.
  • Keep schemas stable and versioned per topic.

ROS3 Development Tips

ROS3 projects prefer git subtree for package control and dependency vendoring.

This keeps builds reproducible and atomic during robotics competition and demo prep as well as during production deployment. Code you use is your responsibility as a developer, and it is therefore advantageous to actually have offline local access to all sources depended upon by your project.

Why the following project structure is preferred for ROS3

  • one repo snapshot contains all required code
  • stable and repeatable builds without surprise upstream changes
  • easy rollback by reverting one commit
  • controlled update windows between competition milestones
  • package source stays self-contained under src/packageX/
  • installed artifacts still land in the standard workspace bin/, lib/, include/, and share/ trees - or optionally can be installed globally on minimal unix distributions into standard directories (e.g., /usr/local/)
rose_ws/
  src/
    package1/
      cmd/
        node1/
          main.c
          node1_math.h
          node1_math.c
        node2/
          main.c
      libraries/  # source for exported/installed libraries
        exported_lib1/
          src/
          include/
      internal/
        internal_library_1/
          src/
          include/
      vendor/
        some_external_project_subtree/
      scripts/
        my_tool.py
      docs/
      test/
        check_1.c
        check_1.sh
      README.md
    package2/
      cmd/
      test/
  bin/      # installed node/script executables
  lib/      # installed library files (.so/.a)
  include/  # installed headers (.h)
  share/    # installed config/data/weights
  Makefile  # build script (Makefile or build.ninja preferred)

Treat each src/packageX/ directory as a package source root. Treat rose_ws/ itself as the install prefix.

That means:

  • source code lives under src/packageX/
  • installed executables land in rose_ws/bin/
  • installed libraries land in rose_ws/lib/
  • installed public headers land in rose_ws/include/
  • installed runtime data lands in rose_ws/share/<package>/

Within a package:

  • cmd/ contains executable entrypoints such as nodes and small tools
  • libraries/ contains source for exported libraries that will be installed
  • internal/ contains package-private code used by cmd/ and libraries/, but not installed as public API
  • vendor/ contains vendored dependencies brought in via git subtree
  • scripts/ contains package-local helper scripts
  • docs/ contains package-local documentation and manpage sources
  • test/ contains checks, fixtures, and test scripts

Do not install internal/ headers into the workspace include/ tree. Only install public headers from exported libraries.

Using git subtree

Subtree is a minimal tool for maintaining an internal copy of a git repository in your own git repository.

Add vendored dependencies

PKG=src/package1

git subtree add --prefix "$PKG/vendor/ros3" https://git.sr.ht/~alecgraves/ros3 master --squash
git subtree add --prefix "$PKG/vendor/mirage" https://git.sr.ht/~alecgraves/mirage master --squash
git subtree add --prefix "$PKG/vendor/quicksand" https://git.sr.ht/~alecgraves/quicksand master --squash

Update vendored dependencies (when planned)

PKG=src/package1

git subtree pull --prefix "$PKG/vendor/ros3" https://git.sr.ht/~alecgraves/ros3 master --squash
git subtree pull --prefix "$PKG/vendor/mirage" https://git.sr.ht/~alecgraves/mirage master --squash
git subtree pull --prefix "$PKG/vendor/quicksand" https://git.sr.ht/~alecgraves/quicksand master --squash

Build Target Conventions

The following build command (Makefile/ninja) targets are recommended:

  • all - default, builds executables and libraries
  • cmd - build rose nodes
  • libraries - build rose libraries (.a/.so)
  • check - run sanitizers and checks
  • docs - build docs
  • install - install
  • install-docs - install manpages

When package1 is built and installed into the workspace:

  • cmd/node1/ builds an executable such as rose_ws/bin/node1
  • libraries/exported_lib1/include/ installs public headers into rose_ws/include/
  • libraries/exported_lib1/src/ builds a library into rose_ws/lib/
  • internal/internal_library_1/ is compiled as needed but not installed publicly
  • runtime assets such as config, maps, calibration files, and model blobs should install into rose_ws/share/package1/

A practical convention for runtime assets is:

rose_ws/
  share/
    package1/
      config/
      launch/
      models/
      calibration/
      examples/

Practical notes

  • Prefer one owning package for each vendored dependency to avoid duplicate subtree copies.
  • Keep cmd/*/main.c thin; move reusable logic into libraries/ or internal/.
  • If a helper is only used inside one node, keep it local to that node directory.
  • If a helper is shared across nodes in the package but should not be public, put it in internal/.
  • If other packages are expected to link against it, promote it into libraries/.