Status: Accepted
Date: October 6, 2026 (#483; D2, D3, D6 revised the same day)
Answers: what a package does that needs a C library, after
ADR-321 D15 left no way for a package’s code to run while a
program is built; Part III 13.4’s build.nika, which described one
Related:
ADR-302 (a call into C, and what crosses);
ADR-321 D9, D14-D15 (no foreign code runs while a program is built);
ADR-287 D14 (build-time code touches nothing);
ADR-310 (what a build may read);
ADR-290 (a Rust crate is described before it is called)
How does a package whose Nikaia code calls a C library get that library linked, in every program that uses the package, without the program’s author doing anything and without anything of the package running while it is built?
A package such as a SQLite driver declares extern functions and offers a
Nikaia interface over them. The functions live in a library the linker has to
find. Today nothing says which: a program passes -l sqlite3 to rustc
through RUSTFLAGS by hand.
| Tool | How a C library is linked or built | Does the package’s code run while building? |
|---|---|---|
| Cargo | build.rs calls pkg-config, cc, bindgen; system-deps reads a table but still needs a short build.rs |
yes, with every right, for every dependency |
| Zig | build.zig is Zig code that builds a graph (linkSystemLibrary, addCSourceFiles) a runner executes; Zig compiles C itself (zig cc) |
yes: build.zig may do anything, and every dependency’s runs |
| Go | directives in the source (#cgo pkg-config: sqlite3, #cgo LDFLAGS: -lsqlite3); go build compiles the package’s C files; go generate is never run by the build |
no; and after CVE-2018-6574 (-fplugin in #cgo CFLAGS) only flags on an allow list pass, pkg-config’s output included |
| SwiftPM | Package.swift is Swift; .systemLibrary(pkgConfig:) |
yes, in the operating system’s sandbox on macOS |
From memory, not checked against a version. What transfers: Go’s split - the package says what, the build tool decides how - and its allow list of flags. What does not: a build script in any language, which Zig shows is as powerful as Cargo’s however its own language reads.
| Option | Why it lost |
|---|---|
build.nika, a program run before the build with a std::build API |
Cargo’s build.rs again: a package’s code running with every right while a program is built, which ADR-321 D15 rules out |
| A script that only builds a graph (Zig) | still a program that may do anything; declaring the graph needs no program |
| Flags the package writes freely | a flag can load code into the compiler or linker (-fplugin, -Wl,-plugin, -specs=) |
| Prebuilt binaries per platform in the package | machine code nobody reviews, shipped into every program that uses the package |
| C sources in the package, compiled by the build | see D5 |
No build.nika, no std::build, no program of the package run before or
during a build. What a build needs from a package is data.
extern block names its library, and the manifest says how it is found; both are required.extern(library: "sqlite3") {
opaque type sqlite3 released by sqlite3_close
fn sqlite3_open(path: ref Array[u8], out: ref mut sqlite3?) -> i32
}
[library.sqlite3]
pkg-config = "sqlite3"
The block says what its functions need, beside them; the entry says where it
comes from, and pkg-config is required in it, written out even where it
repeats the name: an entry is written rarely and copied often, and a written
value is one nobody has to know the default of. A block naming a library the
manifest has no entry for, or an entry no block names, is NK1226. A program
that depends on the package links the libraries it names; its author writes
nothing for it. A block without library links nothing of its own.
Rejected: the name in the block alone, found by pkg-config with -l as
a fallback - a fallback that silently loses pkg-config’s search paths on a
machine without it, and a default a reader has to know; the manifest alone,
which separates a function from the library it lives in.
pkg-config, and only through it.The compiler runs the machine’s pkg-config with arguments it forms from the
entry, never with arguments the package writes. A library pkg-config does
not find, or a machine without pkg-config, refuses the build with NK1224,
naming the package, the library and the pkg-config name.
From pkg-config --libs: -l, -L, -pthread, and on macOS -framework
and -F. Any other flag is refused with NK1225, naming it and the package
whose library returned it. Nothing is compiled from C (D5), so no compiler
flag is taken at all.
Compiling C runs nothing of the package where the build chooses the flags: the
preprocessor has no way to start a program. But it reads: #include with an
absolute path reads any file the compiler may read, and shows it in its error
message; C23’s #embed puts it into the object file; a computed
#include MACRO cannot be found by reading the source first. No switch of GCC
or Clang forbids it, so a build would read past ADR-310. Where the
machine that builds is not the one that runs - a CI job with secrets - that is
new reach. And a C compiler is no security boundary against a hostile source.
This is reopened when what a C compilation reads can be bounded.
extern is written without a convention: extern "C" is refused.Changes ADR-302 D1 and ADR-284 D4’s spelling.
extern { fn strlen(s: ref Array[u8]) -> usize } // calling C
extern(library: "sqlite3") { … } // calling C, library named
pub extern fn greet(name: ref String) -> String { … } // called from C
pub extern struct Point { x: f64, y: f64 } // C's layout
The C convention is the only one on every target (ADR-284 D22),
so a subject that can hold one value says nothing. What follows extern in
parentheses is options, as a dsl block’s build-time arguments and a
comptime’s bounds are. extern "C" is NK1227, and the help writes extern.
Should a second convention ever be needed, it comes as a subject before a ;,
extern(X; library: …), and an extern without one stays C.
Rejected: extern "C" library "…", which reserves library everywhere
(ADR-298 D1: no word is contextual); extern(foreign::Abi::C; …),
an enum of one variant written at every block; extern.C, which is no path
this language writes.
sqlx’s do.-sys crate ({ type = "rust" }) still brings its build.rs; that is
Cargo’s, ADR-321 §6.protoc) runs as part of a build - its output is
generated by hand and committed, or the format gets a grammar in Nikaia; no
C library with its own build system (CMake) is built from source.pkg-config
answers for the machine that builds.pkg-config is rarely present.