← Back to writing

Voleta — Getting Serious About Toolchains

Named 'Violet' in French. 742 files across kernel, libc, and a naked GCC/Binutils toolchain built from source. The shift from 'build a kernel' to 'build the tools that build the kernel.'

KairosOS taught me that the cross-compiler is more important than the kernel features. Voleta was the project where I acted on that lesson.

742 files across three repositories: voleta-operating-system-kernel, voleta-custom-libc, and voleta-naked-toolchain. The name is French for violet — because even in systems programming, the aesthetic matters.

The Naked Toolchain

“Naked” means no system headers, no target libc, no assumptions about the host OS. Just GCC and Binutils configured to emit freestanding x86_64 code:

# voleta-naked-toolchain: building GCC for a target that doesn't exist
export TARGET=x86_64-voleta-elf
export PREFIX=/opt/voleta-cross

# Binutils first — the assembler and linker
../binutils-2.37/configure \
    --target=$TARGET \
    --prefix=$PREFIX \
    --with-sysroot \
    --disable-nls \
    --disable-werror

# Then GCC — the compiler
../gcc-11.2.0/configure \
    --target=$TARGET \
    --prefix=$PREFIX \
    --disable-nls \
    --enable-languages=c \
    --without-headers \
    --disable-hosted-libstdcxx

The 742 files in the toolchain repo aren’t kernel code. They’re build scripts, patches, configuration files, and documentation for reproducing the exact GCC/Binutils build from source. This was the shift: the toolchain isn’t something you download and forget. It’s a first-class component of your OS with its own source, its own build process, and its own versioning.

The Sysroot

The sysroot is the directory structure that the cross-compiler uses as its “root filesystem.” When your cross-compiled code does #include <stdio.h>, the compiler looks in $SYSROOT/usr/include/, not in your host system’s /usr/include/. When the linker resolves -lc, it looks in $SYSROOT/usr/lib/.

voleta-sysroot/
  usr/
    include/
      stdio.h      ← your custom headers
      stdlib.h
      string.h
    lib/
      libc.a        ← your custom libc, compiled for x86_64-voleta-elf
      crt0.o        ← C runtime startup (calls main)

Building the sysroot correctly — getting the headers in the right place, the libraries with the right names, the startup code linked in the right order — is the kind of work that’s invisible when it’s done right and catastrophic when it’s wrong. A missing crt0.o means your program has no entry point. A mismatched header means your libc functions have the wrong signatures. A sysroot path error means the compiler silently uses your host’s libc, and your kernel code links against Linux-specific symbols that don’t exist in your OS.

Voleta’s libc vs. KairosOS’s libc

KairosOS’s libc was an afterthought — the minimum needed to compile the kernel. Voleta’s libc was designed as a standalone component with its own repository, its own tests, and its own header organization.

The practical difference was small: both had memset, memcpy, strlen, and a handful of other functions. The architectural difference was significant: Voleta’s libc could be compiled, tested, and versioned independently of the kernel. If I needed to add printf, I could work on the libc repo without touching kernel code.

This separation — libc as an independent project that the kernel and userspace both consume — is how musl and glibc work in the real world. KairosOS taught me that I needed a libc. Voleta taught me how to structure one.

The D Language Detour

Alongside Voleta, I experimented with writing a kernel in D. The repository exokernel-dlang was an attempt at an exokernel architecture — a kernel that exposes hardware resources directly to userspace programs, letting them make their own abstraction decisions instead of imposing POSIX-style abstractions.

D was attractive for kernel development because of its metaprogramming: compile-time function evaluation, mixins, and templates that are more powerful than C macros. You can generate syscall dispatch tables, interrupt handlers, and memory-mapped I/O wrappers at compile time without code generation scripts.

What D couldn’t do: run without a runtime. D’s standard library assumes a hosted environment — an OS with threads, memory allocation, and I/O. Even @nogc D code (which avoids the garbage collector) still depends on runtime support for exception handling, module initialization, and type information. Removing those dependencies required fighting the language at every step.

The exokernel experiment was valuable for the architecture ideas — capability-based resource management, which would later appear in TerranoxOS — but D wasn’t the right language for the job. A kernel needs a language that assumes nothing, and D assumes an OS exists.

The Lesson

The toolchain is not a dependency — it’s a first-class component of your OS. KairosOS treated the cross-compiler as a build prerequisite: download it, configure it, forget about it. Voleta treated it as a deliverable: documented, versioned, reproducible from source. This is the difference between “I built a kernel” and “I built a kernel development environment.”

The naked toolchain approach also made it possible to iterate faster. When you understand your toolchain deeply — what flags it supports, what it links by default, where it looks for headers — you spend less time fighting mysterious linker errors and more time writing kernel code.


This is Part 4 of 8 in the OS Kernel Museum series.