Build System

basalt has no Cargo.toml or CMakeLists.txt. The entire build is driven by Meson via direct invocations of rustc, clang, and lld through custom_target rules. This page walks through the four steps that produce libc.so, the optional libc++ build, and the build options that gate everything.

Top-Level Entry Point

The basalt subtree is entered from lib/basalt/meson.build:

subdir('c')

_libcxx_opt = get_option('build_libcxx')
_libcxx_src_dir = meson.project_source_root() / 'toolchain' / 'llvm-project' / 'libcxx' / 'src'
_build_libcxx = (_libcxx_opt == 'true') or
                (_libcxx_opt == 'auto' and fs.is_dir(_libcxx_src_dir))
if _build_libcxx
  subdir('cpp')
endif

build_libcxx accepts auto (build if the LLVM submodule is present), true (require it), or false (never build). auto is the default — checking out SaltyOS without the LLVM submodule still gets a working libc.so, but no libc++.so.

basaltc Pipeline

lib/basalt/c/meson.build runs four custom_target rules in dependency order:

Diagram

Step 1: rustc → basaltc.o + basaltc.rmeta

basaltc_rust = custom_target('basaltc_rust',
  input: files('src/lib.rs'),
  output: ['basaltc.o', 'basaltc.rmeta'],
  command: [rustc,
    rust_userland_args,
    '--emit=obj=@OUTPUT0@,metadata=@OUTPUT1@,dep-info=@DEPFILE@',
    '--crate-name=basaltc',
    '--crate-type=lib',
    '--extern', 'trona=' + libtrona_build_dir / 'libtrona.rmeta',
    '--extern', 'trona_posix=' + libtrona_build_dir / 'libtrona_posix.rmeta',
    '@INPUT@',
  ],
  depends: [core, compiler_builtins, libtrona_rust, trona_posix],
)

Key observations:

  • rust_userland_args is the shared userland Rust flag set defined in the top-level Meson build. It includes --edition=2024, --target=x86_64-unknown-saltyos (or aarch64), -C panic=abort, -C opt-level=2, -C relocation-model=pic. The custom rustc target spec is required because the *-unknown-saltyos triples are only known to the patched stage1 compiler in toolchain/rust/.

  • The dual --emit produces both an object file (linked into libc.so) and an .rmeta (used by other Rust crates that need to call basaltc as a Rust crate, including ld-trona.so and selected userland servers).

  • The depends: list pulls in core and compiler_builtins (built once from rust-src at the top level) and the trona crates that basaltc imports.

Step 2: setjmp.S → setjmp.o

setjmp_obj = custom_target('setjmp_asm',
  input: files('src/arch' / arch / 'setjmp.S'),
  output: 'setjmp.o',
  command: [cc_cmd, '--target=' + c_target, '-c', '@INPUT@', '-o', '@OUTPUT@'],
)

setjmp.S and longjmp save and restore the callee-saved register set on each architecture. Pure assembly because Rust cannot represent the register-level control flow of longjmp portably. Linked into libc.so so any caller of setjmp resolves the symbols dynamically.

Step 3: crt_start.S → crt_start.o

crt_start_obj = custom_target('crt_start_asm',
  input: files('src/arch' / arch / 'crt_start.S'),
  output: 'crt_start.o',
  command: [cc_cmd, '--target=' + c_target, '-c', '@INPUT@', '-o', '@OUTPUT@'],
)

crt_start.S defines _start, the ELF entry point. It sets up the initial stack frame, parses argv/envp/auxv, and tail-calls __libc_start_main (defined in src/crt.rs). This object is deliberately not linked into libc.so: each application binary’s link command pulls crt_start.o in directly. If crt_start.o were inside libc.so, the shared library would carry an unresolved reference to main, which would fail at link time for any executable that does not provide main (e.g., shared libraries themselves). See CRT Startup for the runtime side.

Step 4: lld → libc.so

libc_so = custom_target('libc_so',
  input: [basaltc_obj, setjmp_obj, core_obj, compiler_builtins_obj],
  output: 'libc.so',
  depends: [libtrona_so],
  command: [cc_cmd,
    '--target=' + c_target,
    '-shared',
    '-nostdlib', '-nostartfiles',
    '-fno-stack-protector',
    '-Wl,-soname,libc.so',
    '-T', basaltc_link,
    '-L', libtrona_build_dir,
    '-o', '@OUTPUT@',
    '@INPUT@',
    '-ltrona',
  ],
)

cc_cmd is clang with -fuse-ld=lld already in scope; the link is fully self-contained. Notable flags:

-shared

Build a position-independent shared library.

-nostdlib -nostartfiles

Do not link in any host libc, host crt1, host startfiles. We provide our own.

-fno-stack-protector

basaltc does provide a __stack_chk_fail (in stack_protector.rs), but the link does not request canary insertion at compile time.

-T basaltc.ld

Per-architecture linker script (lib/basalt/c/arch/{x86_64,aarch64}/basaltc.ld). It page-aligns .text, .rodata, .data, and .bss, keeps .init_array / .fini_array for global C++ constructors and atexit hooks, and discards .comment, .note., and .eh_frame.

-ltrona

Records libtrona.so as a DT_NEEDED so the runtime dynamic linker brings trona in before basaltc resolves any symbols.

After Step 4, libc.so is a complete shared library: stdio, malloc, pthread, sockets — all the symbols any port could ask for, with DT_NEEDED libtrona.so for the system call layer.

libc++ Pipeline

lib/basalt/cpp/meson.build is more elaborate. It compiles 48 libcxx sources, 18 libcxxabi sources, 2 libunwind C++ sources, 2 C sources, and 2 assembly sources, then links the entire result with lld.

Diagram

Per-Source custom_target

Each .cpp file becomes a single custom_target:

foreach src : _cxx_srcs
  _name = 'cxx_' + src.replace('/', '_').replace('.cpp', '')
  _all_objs += custom_target(_name,
    output: _name + '.o',
    depfile: _name + '.d',
    command: [cc_cmd,
      _cxx_flags,
      '-MMD', '-MF', '@DEPFILE@',
      '-c', _libcxx_src / src,
      '-o', '@OUTPUT@',
    ],
  )
endforeach

The pattern repeats for _cxxabi_srcs, _libunwind_cpp_srcs, _libunwind_c_srcs, and _libunwind_asm_srcs. Per-source custom_target is verbose but gives Meson maximum parallelism and per-file dependency tracking via the depfile.

Compile Flag Set

_cxx_flags is the canonical libc++ flag bundle:

--target=<arch-saltyos>
-std=c++23
-fPIC
-fno-rtti
-funwind-tables
-ffreestanding
-nostdinc
-nostdlib
-D_LIBCPP_BUILDING_LIBRARY
-DLIBCXX_BUILDING_LIBCXXABI
-DLIBCXXABI_BAREMETAL
-DLIBCXXABI_SILENT_TERMINATE
-isystem <cpp source dir>          # __config_site, __assertion_handler
-isystem <libcxx/include>
-isystem <libcxxabi/include>
-isystem <clang resource dir>      # freestanding stddef/stdint/float/stdarg
-isystem <basaltc/include>
-include <__config_site>

Two details deserve attention:

  • -fno-rtti is in _cxx_flags but is stripped from _cxxabi_flags before the libcxxabi compile loop. libcxxabi is the RTTI implementation (dynamic_cast, typeid, the typeinfo objects themselves), so it must be compiled with RTTI enabled. Mirrors upstream LLVM CMake behavior.

  • -nostdinc blocks all host system headers, not just C++ headers. The -isystem chain provides every header explicitly: SaltyOS configuration → libcxx → libcxxabi → clang resource → basaltc. This guarantees that no host libc pollution leaks into the build.

libunwind

libunwind has three flag sets because it mixes languages: _libunwind_flags for .cpp files (no exceptions, freestanding C23), `_libunwind_c_flags` for `.c` files (`-std=c11` instead of `-std=c23`), and a minimal flag set for .S files (just --target and -fPIC). Six sources total: libunwind.cpp, Unwind-EHABI.cpp, UnwindLevel1.c, UnwindLevel1-gcc-ext.c, UnwindRegistersRestore.S, UnwindRegistersSave.S.

Linking libc++.so

libcxx_so = custom_target('libcxx_so',
  input: _all_objs,
  output: 'libc++.so',
  command: [cc_cmd,
    '--target=' + c_target,
    '-shared',
    '-nostdlib', '-nostartfiles',
    '-fuse-ld=lld',
    '-Wl,-soname,libc++.so',
    '-z', 'max-page-size=4096',
    '-o', '@OUTPUT@',
    '@INPUT@',
    _clang_rt_builtins,
    '-L', basaltc_build_dir, '-lc',
    '-L', libtrona_build_dir, '-ltrona',
  ],
  depends: [libc_so, libtrona_so],
)

_clang_rt_builtins is found at build time via clang --target=…​ -print-libgcc-file-name. It points at libclang_rt.builtins-<arch>.a, which provides 128-bit float helpers (lttf2, trunctfdf2, etc.) used by charconv.cpp. Following Fuchsia’s approach, basalt links the builtins statically into each shared library that needs them rather than relying on a system-wide compiler-rt.

DT_NEEDED entries on the resulting libc++.so: libc.so, libtrona.so.

Build Options

Option Default Effect

build_libcxx

auto

auto builds libc++ if the LLVM submodule is present, otherwise skips. true requires the submodule; false never builds.

arch

x86_64

Architecture for the entire SaltyOS build, including basalt. Selects the rustc target spec, the assembly variant of crt_start.S and setjmp.S, the linker script, and the libc++ --target flag.

basalt does not own per-component options. The flags it consumes (rust_userland_args, cc_cmd, c_target) are defined at the top-level meson.build and shared with kernite, trona, and userland.

Adding a New basaltc Source File

Adding a new top-level Rust module to basaltc takes one source change and one rebuild step:

  1. Create the new .rs file in lib/basalt/c/src/.

  2. Add pub mod my_new_module; to lib/basalt/c/src/lib.rs.

  3. Run just distclean && just setup && just build.

The distclean is necessary because lib.rs is the only source file declared in c/meson.build. Meson re-resolves the dependency graph from lib.rs on each rustc invocation, but the top-level discovery happens at meson setup time.

Adding a new C header is even simpler: drop it into lib/basalt/c/include/ (or a subdirectory) and run just build. The header collection step uses a find at setup time, so changes to existing headers do not require re-setup, but adding a new header file does.

For new libc++ sources, edit the cxx_srcs, _cxxabi_srcs, or _libunwind*_srcs lists in lib/basalt/cpp/meson.build and rebuild.

  • Architecture — module map and dependency direction

  • CRT Startup — what the crt_start.o produced here actually does

  • xref:cxx/cxx-runtime.adoc[libc Runtime] — what the `libc.so` produced here exposes

  • libc++ Configuration — the __config_site referenced via -include