Build outputs and native linking.
silk build turns a package into LLVM IR, then hands it to Clang to produce the file [package].kind and [build].stage ask for. Every file lands under the output directory, in a folder per target and profile.
The build/ layout#
build/
llvm/<target>/<profile>/<file> silk build, silk run
llvm/<target>/<profile>/<file>.symbols symbol map (builds with debug information)
llvm/<target>/<profile>/<file>.debug.wasm the module with its DWARF (wasm32-unknown-unknown executables with debug information)
llvm/<target>/<profile>/<file>.map source map of that module (the same builds)
llvm/<target>/<profile>/<file>.inputs the files the build read, to skip an unchanged rebuild
llvm/<target>/<profile>/<file>.units/ cached compiled code, reused by the next build
llvm/<target>/<profile>/<file>.native/ objects compiled from [native].sources
test/llvm/<target>/<profile>/<name> silk test runner
cache/<package>/ the checker's cache, per package
tools/<package>/<step>/ output of a build.silk tool step
documentation.json silk doc
documentation/<member>.json silk doc at a workspace rootbuildis[build].output-dir, or[workspace].output-dirfor workspace members. See Workspaces.<target>is the full target name, such asx86_64-unknown-linux-gnu.<profile>isdebug,release,release-with-debug,release-sizeorrelease-size-with-debug, or the name of the selected[profiles.<name>]. See Targets and profiles.The
.inputs,.units,.nativeandcacheentries only make the next build faster. Deleting them is safe.A
wasm32-unknown-unknownexecutable built with debug information ships without its DWARF. The module names<file>.debug.wasmin anexternal_debug_infosection and<file>.mapin asourceMappingURLsection, so serve them beside it for a browser's debugger. See Debugging.silk cleandeletes the whole output directory.
File names#
[package].kind | stage = "final" (default) |
|---|---|
executable | <name> |
library | none: the package is checked |
static-library | lib<name>.a |
shared-library | lib<name>.so, lib<name>.dylib on macOS (not built yet) |
object | <name>.o |
[build].stage | File | What Clang does |
|---|---|---|
llvm-ir | <name>.ll | nothing; the IR as text |
llvm-bitcode | <name>.bc | -c -emit-llvm |
assembly | <name>.s | -S |
object | <name>.o | -c |
final | as above | compiles and, for an executable, links |
<name> is [package].name, hyphens kept: libtext-utils.a. A non-final stage stops before linking, so it works for any target. For a single file, silk build <source> -o <output> --emit <stage> does the same. With stage = "assembly" in [build], the package from silk init hello builds hello.s:
Compiling hello (x86_64-unknown-linux-gnu, debug)
load 143 modules, 1 in package 0.06s
check 1 module 0.14s
codegen 689 functions 0.18s
borrow 229 bodies 0.01s
link clang 0.29s
Finished build/llvm/x86_64-unknown-linux-gnu/debug/hello.s in 0.71sThe link row is the Clang step whatever the stage; for assembly it runs clang -S.
The symbol map#
Each output of a build with debug information, such as the default debug profile, has a .symbols file beside it. Each line is a native symbol, its kind and the Silk declaration it comes from, separated by tabs:
main export hello@0.1.0:silk/platform/native/start.silk::_._.enter
silk_e0307b61cbd3a68762f928296596e9cb function hello@0.1.0:silk/platform/native/start.silk::_._.enterKinds are export (a C name), function, drop and helper. The single-file form prints one Compiled line instead of progress: silk build src/main.silk -o hello.s --emit assembly prints Compiled src/main.silk -> hello.s (llvm, x86_64-unknown-linux-gnu, 339 symbols), with both paths relative to the current directory, or in full when a file is outside it. Its symbol count is the number of lines in this file.
Finding Clang#
| Source | Used for |
|---|---|
--clang <path or name> | silk build, silk run and silk test on a package, and single-file silk build <source> -o <output>. A bare name is looked up on PATH. |
SILK_CLANG | Every build without --clang, when set. |
clang on PATH | Otherwise. |
Use Clang 22 or newer. Silk hands Clang LLVM 22 bitcode, and a build refuses an older Clang before compiling.
Static archives use
SILK_LLVM_ARwhen set, else thellvm-arin Clang's directory.Clang runs with only
PATH,HOME,TMPDIR,TMP,TEMP,LANG,LC_ALLandLC_CTYPEfrom your environment.CFLAGS,LDFLAGSand similar are not passed.An executable link adds
-lc -lmafter everything else.On Linux, an executable links with the
ld.lldin Clang's installation directory (theInstalledDirline ofclang --version) when there is one, through--ld-path. Otherwise Clang uses the system linker. Aplatform-supplythat names a linker, and a no-libc build, keep their own linker. The progress row readslink clangeither way.On Linux, an executable is not position independent unless its profile sets
relocation = "pic": the default links with-fno-picand-no-pie. See Targets and profiles.
| Problem | Message |
|---|---|
No clang on PATH | silk: cannot find clang on PATH; set SILK_CLANG or pass --clang the path of the Clang executable |
--clang name not found | silk: cannot find nosuchclang on PATH; pass --clang the path of the Clang executable |
| The chosen Clang does not start | silk: cannot run the C compiler /nope/clang (from SILK_CLANG); set SILK_CLANG or pass --clang the path of the Clang executable. The parenthesis is from SILK_CLANG, from --clang or found on PATH. |
Each of these fails the target and exits 2. silk build and silk run print the message, then a link failed row and a Failed line.
macOS SDK#
On macOS the link needs an SDK. The compiler resolves it once per build, in this order:
[native].platform-supply, when the manifest sets one.SDKROOT, when set. An invalidSDKROOTfails; it never falls back.xcrundiscovery, for the host target only.
The SDK reaches Clang as -isysroot, with -mmacosx-version-min from the profile's deployment. Linux needs no SDK.
[native].platform-supply takes one of:
| Value | Meaning |
|---|---|
{ kind = "automatic" } | The default: the order above. |
{ kind = "native" } | Host discovery, named explicitly. |
{ kind = "explicit", target = "...", root = "...", linker = "...", origin = "...", support = [...] } | An installed SDK or sysroot at root (-isysroot on macOS, --sysroot on Linux), the linker at linker (--ld-path), and optional GCC support installations, each { root, target, origin } (--gcc-install-dir). origin is a label. |
{ kind = "managed", name = "..." } | Recognized, not supported: UnsupportedProvider: x (project). |
Each kind takes only the keys shown. A key it does not take, here name under kind = "native", is refused at its own line, and so is an extra key in a support entry (native.platform-supply.support.<key>):
[native.platform-supply]
kind = "native"
name = "x"Invalid Silk project manifest /home/you/hello/silk.toml:8: native.platform-supply.name is not a supported fieldAny other shape, such as an unknown kind, a string instead of a table, or an explicit supply for a target it cannot serve, is refused at the line that writes platform-supply, or at the [native.platform-supply] header:
Invalid Silk project manifest /home/you/hello/silk.toml:7: InvalidConfiguration: platform-supply (/home/you/hello/silk.toml:native.platform-supply)Extra native link inputs#
[native].link adds C objects, archives and libraries to the link. Each entry is a table with exactly one kind:
| Entry | Linker argument | Notes |
|---|---|---|
{ object = "native/scale.o" } | the path | |
{ static-archive = "native/libscale.a" } | the path | |
{ search-path = "native" } | -L<path> | |
{ library = "scale", mode = "dynamic" } | -lscale | |
{ library = "scale", mode = "static" } | -Wl,-Bstatic -lscale -Wl,-Bdynamic | not on macOS |
{ linker-script = "link.ld" } | -Xlinker -T -Xlinker <path> | Linux only |
{ framework = "Security" } | -framework Security | macOS only |
Paths are relative to the manifest, with no absolute paths and no
...Inputs go to the linker in the order listed, before
-lc -lm.An
objectartifact merges only objects and archives into its.o. Astatic-libraryadds only objects as archive members.
A C function linked into a Silk program:
int scale(int value) {
return value * 3;
}clang -c -fPIC native/scale.c -o native/scale.o[native]
link = [{ object = "native/scale.o" }]unsafe extern "C" fn scale(value: i32) -> i32
pub fn main() -> i32 {
return unsafe scale(14)
}silk run; echo "exit $?"exit 42A bad entry stops the build with exit status 1 and names the manifest and the entry's index. A path must stay inside the package, so an absolute path, or one with . or .. segments, is refused on purpose:
silk: invalid native link input: /home/you/cmix/silk.toml: [native].link[0]: object /tmp/abs.o must be a relative path below the manifest's directory, without . or .. segments, so the package names only its own filesAn entry with no kind, or with two, ends in an entry is exactly one object, static-archive, search-path, linker-script or framework value, or one library with mode "static" or "dynamic". A bad mode ends in a library's mode must be "static" or "dynamic". A file that does not exist is reported by Clang at link time, clang: error: no such file or directory: '/home/you/cmix/native/missing.o', and the build exits 1.
An entry the target cannot link, such as { framework = "Security" } on Linux, is refused when the build reaches the link. silk check accepts it. silk build checks and generates code, then fails that package's link and exits 1:
Compiling cmix (x86_64-unknown-linux-gnu, debug)
load 135 modules, 1 in package 0.06s
check 1 module 0.09s
codegen 484 functions 0.13s
borrow 147 bodies 0.00s
[x86_64-unknown-linux-gnu]
silk: invalid native link input: /home/you/cmix/silk.toml: [native].link[0]: framework Security links only on macOS targets
link failed 0.02s
Failed cmix in 0.33sSee Unsafe code and C interop in the reference for extern "C".
Per-platform entries#
Any entry may add os, one of "macos", "linux", "windows" and "wasi", or a list of them. A build for another operating system skips the entry instead of refusing it, so one manifest declares each platform's libraries and frameworks:
[native]
link = [
{ library = "m", mode = "dynamic" },
{ framework = "Metal", os = "macos" },
{ library = "stdc++", mode = "dynamic", os = ["linux", "wasi"] },
]Inputs of dependencies#
A library's [native].link reaches every executable that depends on it, directly or through other packages, so the executable's manifest repeats none of it.
The executable's own inputs come first, then each dependency's, with every package before the packages it depends on, so an archive follows the objects and archives that use it.
An input that several packages list (the same file, the same library with the same mode, or the same framework) links once, at its last place.
A
static-libraryorobjectartifact links only its own manifest's inputs.
Compiling C sources#
[native].sources lists C, Objective-C, C++ and Objective-C++ files of the package. The build compiles them with the same Clang it links with, so there is no separate clang -c step to remember:
[native]
sources = [
{ c = "native/*.c", include = ["native/include"], flags = ["-DSHIM_VERSION=2", "-Wall"] },
{ objc = "native/metal/*.m", flags = ["-fno-objc-arc", "-fblocks"], os = "macos" },
]| Key | Meaning |
|---|---|
c, objc, cpp, objcpp | Exactly one: the language and the files, as a path or a list of paths relative to the manifest's directory. A * in the last segment matches the files of that directory, in name order; a pattern that matches nothing is refused |
include | Directories of the package to search for headers |
flags | Extra Clang flags, from an allowlist |
os | The systems the entry applies to, as for link |
The objects link ahead of the package's
linkentries and reach dependent executables the same way, so a dependency's C code compiles without any setup in your package. Astatic-libraryartifact archives them.Each file compiles with the build's target, optimization, debug information, CPU and platform supply.
flagstakes defines (-D,-U), warnings (-W,-w,-pedantic), a language standard (-std=), optimization (-O), debug information (-g) and code generation flags (-f,-m). A flag that hands arguments to another tool (-Wl,,-Wa,,-Wp,,-mllvm), loads code, or names a file for Clang (-fplugin,-I,-include,-B,@file, sanitizer and profile lists) is refused, naming the manifest line. Useincludefor header directories.
Objects are cached in <file>.native beside the output, named by a digest of the Clang, the flags, the file and the package headers it includes. An edit to one file or header compiles only the objects it reaches, a build that changes none of them starts no Clang at all, and silk check compiles nothing.
Prebuilt archives#
[native.archive.<name>] pins a prebuilt archive, such as a vendor's SDK, by url and sha256. A path written <name>:<path> in link or sources names a file inside it, so the package uses the SDK without copying it in:
[native]
link = [
{ static-archive = "dawn:lib/libwebgpu_dawn.a", os = "macos" },
{ static-archive = "dawn:lib64/libwebgpu_dawn.a", os = "linux" },
]
[native.archive.dawn.targets.aarch64-apple-darwin]
url = "https://github.com/google/dawn/releases/download/<version>/Dawn-<commit>-macos-latest-Release.tar.gz"
sha256 = "d4f419dcbe81de4b86e6fbf172b647f41c67820a925efca55dae3bd43248d8e2"
strip = 1| Key | Meaning |
|---|---|
url | https://, or file:/// for a local mirror |
sha256 | The file's SHA-256, 64 hexadecimal digits |
strip | Leading path segments to drop when extracting. Optional |
targets.<target> | The same keys for one target, when each target has its own file |
A build that links the package fetches the file once into
$SILK_HOME/archives/<sha256>(~/.silk/archivesby default), checks its SHA-256 before reading it, and extracts it if it is a tar file, gzip-compressed or not. Later builds reuse the extraction.Extraction writes only directories and regular files, skips links, and refuses a member path that is absolute or holds
...silk checkfetches nothing. WithSILK_OFFLINEset, a build that needs an archive it does not have fails, naming it.Only the declaring manifest may use its archives, and two packages of one build may not declare the same name. A build for a target the archive has no
urlfor refuses the reference.
Using your own copy#
To build against a local checkout of the SDK instead, override the archive with a directory. In the root manifest:
[native.override.dawn]
path = "../dawn-build"or, for one machine, in $SILK_HOME/native-overrides.toml. An override is never fetched, a relative path is resolved against the file that names it, and the machine's file wins over the root manifest. A dependency's overrides are ignored.
Function order#
[native].function-order names a text file of symbols that a Linux or macOS executable places first, in the order listed, so that code which runs together shares memory pages:
[native]
function-order = "order.txt"# hot path first
silk_f0247e45df317956d0904dd2e95b51a8
mainOne symbol per line, as the symbol map or
nmspells it. Blank lines and lines starting with#are skipped. On macOS the compiler adds the leading_itself.Linux links with function sections and passes the file to lld as
--symbol-ordering-file; macOS passes it as-order_file. A wasm32 module ignores it.The path follows the same rule as
[native].link:silk: invalid native link input: /home/you/hello/silk.toml: [native].function-order must be a relative path below the manifest's directory, without . or .. segments, so the package names only its own files(exit 1).A file that cannot be read stops the link:
silk: cannot read the function order file /home/you/hello/order.txt that [native].function-order names(exit 2).Editing the file relinks the executable, with no other recompilation.
Runtime composition (advanced)#
An executable starts in a runtime module that sets up the process and calls main. Without [unstable].composition, the compiler takes it from the std lib's catalog (compositions.json):
| Targets | libc | Runtime |
|---|---|---|
aarch64-apple-darwin | system | silk/platform/native/start (hosted) |
| Linux x86-64 and arm64 | gnu | silk/platform/native/start (hosted) |
wasm32-unknown-unknown | none | silk/platform/wasm/start (standalone) |
wasm32-wasip1, wasm32-wasip2 | none | silk/platform/wasi/start (wasi) |
[unstable].composition replaces the catalog for one package. Its keys:
| Key | Meaning |
|---|---|
runtimes | [{ name = "...", module = "silk/..." }]: runtimes this package may use. |
defaults | Exactly one runtime name, used unless the profile's runtime names another. An executable needs one. Each entry must be a string: unstable.composition.defaults entries must be runtime names. A name not listed in runtimes is refused at its line: unstable.composition.defaults entry "main" names no runtime in unstable.composition.runtimes; it lists "start", or it lists none. |
components | [{ capability = "execution-storage", bindings = [{ operation = "...", module = "...", declaration = "..." }] }]: the create, acquire, release and destroy operations of execution storage, bound to module declarations. execution-storage is the only capability; any other is refused, as in unstable.composition.components.capability = "other" is not supported by the native compiler; the only capability is "execution-storage". An operation other than those four is refused at its line: unstable.composition.components.bindings.operation = "frobnicate" is not an operation; use create, acquire, release or destroy. So is one that is not a string (unstable.composition.components.bindings.operation must be a string: create, acquire, release or destroy), and an entry with no operation at the entry's line (unstable.composition.components.bindings entry has no operation; use create, acquire, release or destroy). |
retention | [{ module = "...", declaration = "..." }]: extra declarations kept in the output. |
requirements | Must be empty: unstable.composition.requirements is not supported by the native compiler yet. |
Any other key is refused at its line, as in Invalid Silk project manifest /home/you/hello/silk.toml:7: unstable.composition.bogus is not a supported field. So is a key the entries of these lists do not take, even inside an array written over several lines:
[unstable]
composition = { runtimes = [
{ name = "synchronous", module = "silk/platform/native/start_sync" },
{ name = "hosted", modul = "silk/platform/native/start" },
], defaults = ["synchronous"] }Invalid Silk project manifest /home/you/hello/silk.toml:9: unstable.composition.runtimes.modul is not a supported field; did you mean unstable.composition.runtimes.module?Line 9 is the line that writes modul. A misspelled key in a retention entry is refused the same way (unstable.composition.retention.decl is not a supported field), and in a component's bindings entry as unstable.composition.components.bindings.<key>.
For example, silk/platform/native/start_sync is a smaller hosted runtime for a main that returns i32 and needs only HostInput:
[unstable]
composition = { runtimes = [{ name = "synchronous", module = "silk/platform/native/start_sync" }], defaults = ["synchronous"] }import silk.system.host_input { HostInput, HostInputError }
import silk.num.usize
pub effect fn main() -> i32 ! HostInputError ? &mut HostInput {
let count = run HostInput.argumentCount()
if count == usize.ONE {
return 0
}
return 3
}silk run -- a b; echo "exit $?"exit 3A main the runtime cannot start is SEM0225. Here main is a plain pub fn main() -> i32:
Compiling sync (x86_64-unknown-linux-gnu, debug)
load 123 modules, 1 in package 0.07s
check 1 module 0.05s
codegen failed: 117 functions, 1 error 0.01s
[x86_64-unknown-linux-gnu]
src/main.silk:1:8: error[SEM0225] Entry point `main` has a result or requirement the selected runtime `silk/platform/native/start_sync` cannot start
Failed sync in 0.15sA composition that leaves no single runtime, such as one with two runtimes and no defaults, is silk: the build selects no single runtime (ARTIFACT-001). A std lib runtime selected for a target its catalog entry does not list, such as silk/platform/native/start for wasm32-wasip1, is refused before anything compiles, and the message names the runtime to use: silk: the runtime module silk/platform/native/start does not support the target wasm32-wasip1; list silk/platform/wasi/start in [unstable.composition].runtimes and select it for this profile with `runtime = { kind = "named", name = ... }` (ARTIFACT-001). A default that runtimes does not list is refused earlier, at its line in silk.toml (see the defaults key above). Read a runtime module's page in the Std lib API reference before you select it. Linux without libc in Targets and profiles shows the silk/platform/linux/start case.
Building without a runtime#
A profile with runtime = { kind = "none" } builds with no runtime module, so nothing calls the Silk main. The program starts in a C main that you export:
[build]
profile = "bare"
[profiles.bare]
target = "x86_64-unknown-linux-gnu"
runtime = { kind = "none" }export "C" fn start() -> i32 as "main" {
return 7
}silk run; echo "exit $?"exit 7An executable that exports no C main is refused before Clang runs, with exit status 2. Here main is a plain pub fn main() -> i32, and silk build prints:
Compiling tiny (x86_64-unknown-linux-gnu, bare)
load 116 modules, 1 in package 0.00s
check 1 module 0.00s
codegen 0 functions 0.00s
borrow 1 body 0.00s
[x86_64-unknown-linux-gnu]
silk: an executable built with runtime = { kind = "none" } has no `main`; export one, as `export "C" fn start() -> i32 as "main"` does, or select a runtime
Failed tiny in 0.01ssilk checkdoes not look for the Cmain; only an executable build does.An
objectorstatic-libraryartifact, and a non-final stage, need nomain.When the package has native link inputs, one of them may define
main, so the compiler leaves the check to the linker. A link without one fails with Clang'sundefined reference to `main'and exit status 1.
See also#
The manifest, Targets and profiles, Libraries and exports, Workspaces, Build plans and generated code; Unsafe code and C interop and Program entry and termination in the reference.