TAIL OSv0.9.0

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

  1. Prerequisites
  2. Run TailOS
  3. Developer setup
  4. Developing Applications
  5. Troubleshooting

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 all runs direnv allow for this worktree automatically. You only run direnv allow yourself 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_release toolchain. 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 registers tail_release with rustup and needs no external cross-compiler and no ~/.cargo/config.toml link flags — the aarch64-unknown-tail target links with the toolchain's own bundled rust-lld. Then source <dir>/tail-sdk-env.sh and build with cargo +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.py and 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 mount it by hand? tail_disk.img is 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.py does all of that correctly; a hand-rolled mount -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-release re-deploys the OS's own utilities but leaves your hello-tail in place. make clean (or any change that recreates the disk image) removes it — just re-run the deploy_disk.py step 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

  1. Start Simple: Begin with basic applications before complex ones
  2. Use Debug Builds: Debug builds include more error checking
  3. Test Frequently: Test your applications regularly during development
  4. Read Documentation: Familiarize yourself with tail_core API
  5. 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.

Generated from doc/get_started.md in the TAIL OS repository.