Getting Started with TAIL OS
Welcome to TAIL OS! This guide helps you run TailOS, set up a development environment, and build your own applications.
Working on TAIL OS itself (kernel, drivers, build system) rather than just running it? See Developer setup below — you want utility/host/dev_setup/dev_setup.sh, not the prebuilt-image launcher.
Table of Contents
Prerequisites
System Requirements
- Operating System: Ubuntu 22.04 or 24.04 (22.04 is the validated baseline for building from source — the toolchain build pins
gcc-11) - Architecture: x86_64 host system
- Memory: 4 GB to run the prebuilt image; 8 GB recommended to build from source
- Storage: ~10 GB to run the prebuilt image; ~30 GB to build from source (toolchain build)
- Required Software: see below — running the prebuilt image needs only QEMU; building from source installs every dependency for you
To run the prebuilt image you only need QEMU — installed in step 1 of Run TailOS below.
To build from source, do not hand-install build dependencies — the
developer bootstrap (Developer setup) installs all of them
(build-essential, bison, flex, texinfo, the GMP/MPC/MPFR/ISL/elf-dev
libraries, cmake, ninja-build, gdb-multiarch, git-lfs,
gcc-11/g++-11, QEMU, …) for you.
Run TailOS
Boot TailOS in QEMU in about two minutes — no toolchain, no build, no clone.
1. Install QEMU (the only requirement):
sudo apt install -y qemu-system-aarch64 qemu-utils
2. Launch it with the one-command launcher. On first run it downloads the
prebuilt kernel (tail.rfs) and data disk (tail_disk.img) into
~/.cache/tailos, caches them for next time, and boots QEMU with the right
flags:
curl -sSL (private repository) | bash
Prefer to read the script first? Clone the repo and run it locally instead:
./scripts/run_tailos_qemu.sh
3. You're in. After a few seconds — the FAT disk can take up to a minute on the very first boot — the boot log finishes at the TailOS shell prompt:
[START] kernel
[SUCCESS] KERNEL_MEMORY_ALLOCATOR is initialized
[SUCCESS] exception manager is initialized
[SUCCESS] system clock is initialized
# Type 'exit' to terminate TSH #
/$
Try a few things, then exit:
/$ help
/$ ls /usr/bin
/$ /usr/bin/pidls
Exit QEMU with Ctrl-A, then X.
- Re-download fresh images (e.g. after a new release):
rm -rf ~/.cache/tailos.- No output, or it hangs before
/$? See install_qemu.md.
Developer setup
Want to change TailOS, or build apps against the current source? The developer bootstrap turns a fresh Ubuntu machine into a complete TailOS build environment with one script. You run it once; it takes ~2–3 hours, mostly unattended (the bulk is building two custom Rust toolchains).
Before you start: Ubuntu 22.04 (x86_64), ~30 GB free disk, 8 GB+
RAM, and a network connection. You'll be prompted for sudo once, to install
apt packages.
1. Install the two things the script can't self-install, then clone:
sudo apt install -y git make
git clone (private repository) ~/src/tailos
cd ~/src/tailos
2. Run the bootstrap (grab a coffee — this is the ~2–3 hour part):
./utility/host/dev_setup/dev_setup.sh all
It installs every build dependency, builds the aarch64-elf cross-compiler
(needed only to build the custom Rust toolchain — not to build or run the OS or
apps) and the tail_release / tail_debug Rust toolchains, and wires up your
shell (~/.bashrc + direnv, which sets this worktree's TAIL_PREFIX install
prefix). Every step is idempotent — if one fails (say, a flaky download), just
re-run it, or run … all again.
3. Reload your shell, then build and boot:
exec bash # picks up TAIL_PREFIX + the direnv hook
make build-release && make run-release # builds everything, then boots QEMU
When QEMU reaches the /$ prompt, your environment works. Exit with
Ctrl-A, then X — you're ready to develop applications.
dev_setup.sh allrunsdirenv allowfor this worktree automatically. You only rundirenv allowyourself in additional worktrees you create later.
Need to re-run a single step, or recover a half-finished toolchain build?
Every subcommand (apt_deps, cross_compiler, rust_tail_release, …) can be
run on its own — see the
dev_setup reference for the full list and
recovery recipes.
Developing Applications
A TailOS application is an ordinary Rust std program — a normal fn main()
with the full standard library. Only the kernel is no_std. You compile it for
the aarch64-unknown-tail target with the tail_release toolchain, copy the
binary onto the TailOS data disk, and run it from the shell at /usr/bin.
Prerequisite: the
tail_releasetoolchain. If you only want to build apps, the quickest way to get it is the TailOS SDK: extract the SDK bundle and run./install.sh --prefix <dir>, which registerstail_releasewithrustupand needs no external cross-compiler and no~/.cargo/config.tomllink flags — theaarch64-unknown-tailtarget links with the toolchain's own bundledrust-lld. Thensource <dir>/tail-sdk-env.shand build withcargo +tail_release build --target aarch64-unknown-tail. If you are building TailOS from source, Developer setup gives you the same toolchain.
This walk-through builds an out-of-tree app (your own crate, anywhere on disk).
If instead you want to contribute a utility that ships with the OS, add a crate
under utility/target/ and wire it into the Makefile and tail.build the way
the existing utilities (ls, pidls, top, …) do.
1. Create the project
cargo new hello-tail
cd hello-tail
The default src/main.rs already works unchanged:
fn main() {
println!("Hello, TailOS!");
}
2. Select the TailOS toolchain
Pin the custom toolchain for this project directory (sub-directories inherit it):
rustup override set tail_release
Equivalently, pass +tail_release to each cargo invocation.
3. Build for the TailOS target
cargo build --target aarch64-unknown-tail --release
No extra linker flags are needed: the aarch64-unknown-tail target spec bakes
the whole user-space link recipe — the link script, the self-contained
tail_crt0.o start object, and the freestanding link flags — into the target
itself. The build links a bootable binary with an empty ~/.cargo/config.toml
and no external cross-compiler. The binary lands at:
target/aarch64-unknown-tail/release/hello-tail
To shrink it, add an optimized release profile to Cargo.toml (the same one the
bundled utilities use):
[profile.release]
opt-level = "z"
codegen-units = 1
strip = "symbols"
4. Copy the binary onto the TailOS disk
make run-release boots the tail_disk.img in the repository root and mounts
its data partition as /. Add your binary to that image with the same tool the
build uses — no sudo, no manual mounting.
Create a one-line deploy manifest (e.g. deploy.build in the tailos repo):
[disk]
/usr/bin/hello-tail=hello-tail
The left side of = is the path on the TailOS disk; the right side is the
binary's filename inside the directory you pass as the last argument below. Then,
from the tailos repo, run:
python3 utility/host/deploy_disk/deploy_disk.py \
deploy.build \
tail_disk.img \
/path/to/hello-tail/target/aarch64-unknown-tail/release
deploy_disk.py writes hello-tail into /usr/bin/ on the image's FAT data
partition, adding to the files already there rather than erasing them.
File names: TailOS supports VFAT long file names, so
hello-tail(longer than the classic FAT 8.3 limit) works end to end —deploy_disk.pyand the OS file system both write the long-name directory entries, and a unique 8.3 short alias (e.g.HELLO-~1) is generated automatically. Path look-ups are case-insensitive.
Why not
mountit by hand?tail_disk.imgis a two-partition MBR image: the FAT data partition starts at sector 133120 (≈ 65 MiB), not at 1 MiB, and the two FAT copies must be kept in sync.deploy_disk.pydoes all of that correctly; a hand-rolledmount -o offset=…is easy to get wrong (the binary silently won't appear).
5. Run it
From the tailos repo:
make run-release
Wait for the /$ prompt, then:
/$ ls /usr/bin
/$ /usr/bin/hello-tail
Hello, TailOS!
Exit QEMU with Ctrl-A, then X.
Re-running
make build-releasere-deploys the OS's own utilities but leaves yourhello-tailin place.make clean(or any change that recreates the disk image) removes it — just re-run thedeploy_disk.pystep from step 4.
6. Using TailOS services from your app
For anything beyond the standard library — IPC topics, talking to OS servers,
device memory — TailOS std re-exports the platform crate tail_core. Enable it
with the tail_core feature; no extra Cargo dependency is required:
#![feature(tail_core)]
Publish / subscribe (topic IPC)
Topics are asynchronous pub/sub. Publishing never blocks, and read() is
non-blocking — it returns None when no message is queued, so a subscriber polls
on its own schedule:
#![feature(tail_core)]
use std::tail_core::topic::publisher::Publisher;
use std::tail_core::topic::subscriber::Subscriber;
fn main() {
let publisher = Publisher::new("sensor/temperature");
let subscriber = Subscriber::new("sensor/temperature");
let sample: u32 = 42;
publisher.publish(&sample);
// read() is non-blocking — poll until a message arrives.
loop {
if let Some((ptr, size)) = subscriber.read() {
assert!(size >= core::mem::size_of::<u32>());
let value = unsafe { core::ptr::read_unaligned(ptr as *const u32) };
println!("received: {}", value);
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
Calling an OS service (request / reply)
Servers (process_manager, the FAT filesystem, TTY, …) are reached by name
through the service client. This is the exact call pidls makes to query
process_manager for the running-process list:
#![feature(tail_core)]
use std::tail_core::service::client;
use std::tail_core::service::process_manager::PROCESS_MANAGER_LIST_PROCESSES;
use std::tail_core::service::service_request::ServiceRequestData;
fn main() {
// LIST_PROCESSES needs no payload; send a zero-length request.
let dummy: u8 = 0;
let request = ServiceRequestData::new(&dummy, 0);
match client::send_service_request_and_wait_for_reply::<u8, u8>(
"process_manager",
PROCESS_MANAGER_LIST_PROCESSES,
&request,
) {
Ok(reply) => {
// Reply begins with a u64 process count (see ProcessListReply).
let count = unsafe { *(reply.get_reply_message_ptr() as *const u64) };
println!("{} processes running", count);
}
Err(e) => eprintln!("service call failed: {:?}", e),
}
}
The reply buffer and request buffer are released automatically when the
ServiceReplyData / ServiceRequestData values drop (RAII). For the full
reply-parsing example see
utility/target/pidls/src/main.rs; for the
complete client API (including the ≤ 32-byte register fast path
send_request_reg) see
library/tail_core/src/service/client.rs.
Memory
Use the standard heap — Vec, Box, String, etc. — for ordinary allocation;
it behaves exactly as on any std platform. The lower-level
std::tail_core::memory::mmap::mmap_physical_memory(base, size, prot) is for
mapping physical / device memory (MMIO), not general-purpose allocation.
7. Debugging
Build a debug binary and deploy it the same way (step 4), then start QEMU with the GDB stub from the tailos repo:
cargo build --target aarch64-unknown-tail # debug profile (no --release)
# ...deploy with deploy_disk.py as in step 4...
make run-gdb
In another terminal, point gdb-multiarch at your binary and attach — user-space
debugging uses the in-OS debug server on port 1234, the kernel uses 5555:
gdb-multiarch target/aarch64-unknown-tail/debug/hello-tail
(gdb) target extended-remote :1234 # user-space (kernel: target remote :5555)
(gdb) break main
(gdb) continue
println! / eprintln! go to the console, so plain print tracing works too. See
debug.md for more.
Troubleshooting
Common Issues
Build Failures
# Clean and rebuild
make clean
make build-debug
QEMU Issues
# Check QEMU installation
qemu-system-aarch64 --version
# Try different QEMU options
qemu-system-aarch64 -M raspi3b -kernel ${TAIL_PREFIX}/os_image/tail.rfs -serial stdio
Rust Toolchain Issues
# Update Rust
rustup update
# Reinstall target
rustup target remove aarch64-unknown-tail
rustup target add aarch64-unknown-tail
Cross-Compiler Issues
# Rebuild toolchain
cd toolchain/aarch64-elf-gcc
./build_target_toolchain.sh
Getting Help
- Documentation: Check
doc/directory for detailed documentation - Issues: Report bugs and ask questions on the project repository
- Community: Join discussions in project forums
Development Tips
- Start Simple: Begin with basic applications before complex ones
- Use Debug Builds: Debug builds include more error checking
- Test Frequently: Test your applications regularly during development
- Read Documentation: Familiarize yourself with
tail_coreAPI - Use GDB: Debugging is essential for complex applications
This guide provides the foundation for TAIL OS development. For advanced topics, refer to the detailed documentation in the doc/ directory.