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:
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:
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:
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
Startup workflow
Terminal 1 (master):
Terminal 2 (subscriber):
Terminal 3 (publisher):
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.
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:
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.
- 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
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, ×tamp_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/)
Recommended Structure
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/.