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:
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_argsis 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-saltyostriples are only known to the patched stage1 compiler intoolchain/rust/. -
The dual
--emitproduces both an object file (linked intolibc.so) and an.rmeta(used by other Rust crates that need to call basaltc as a Rust crate, includingld-trona.soand selected userland servers). -
The
depends:list pulls incoreandcompiler_builtins(built once fromrust-srcat 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(instack_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_arrayfor global C++ constructors and atexit hooks, and discards.comment,.note., and.eh_frame. -ltrona-
Records
libtrona.soas aDT_NEEDEDso 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.
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-rttiis in_cxx_flagsbut is stripped from_cxxabi_flagsbefore 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. -
-nostdincblocks all host system headers, not just C++ headers. The-isystemchain 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 |
|---|---|---|
|
|
|
|
|
Architecture for the entire SaltyOS build, including basalt. Selects the rustc target spec, the assembly variant of |
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:
-
Create the new
.rsfile inlib/basalt/c/src/. -
Add
pub mod my_new_module;tolib/basalt/c/src/lib.rs. -
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.
Related Pages
-
Architecture — module map and dependency direction
-
CRT Startup — what the
crt_start.oproduced here actually does -
xref:cxx/cxx-runtime.adoc[libc Runtime] — what the `libc.so` produced here exposes
-
libc++ Configuration — the
__config_sitereferenced via-include