std::build
(manifest builder + step graph).
std::build provides helper APIs for writing Silk build modules (build.slk).
Build modules are executed by the silk CLI (see build scripts)
and must produce a TOML v1.0 package manifest in the silk.toml format.
This module is intentionally a tooling surface:
- it does not change the compiler’s manifest-driven build model,
- it exists to make build modules easy to write, deterministic, and hard to get wrong.
Concepts#
Context#
A Context describes the build-module invocation.
package_root: absolute package root directory.action: the current package action string."build"forsilk build --package,silk check --package, andsilk test --package"install"forsilk build install --package"uninstall"forsilk build uninstall --package
The driver provides these values to the build module via the
std::interfaces::Builder entrypoint parameters:
package_root(string)action(string)
For standalone hosted tools (not build modules), Context.from_args(argc, argv)
is available to parse argv into a Context.
Build#
A Build is a programmatic builder for a package manifest. It exposes methods
for setting:
[package]fields (name,version,definitions),[build]fields (default_target,security_provider),- package-level
[[native]]entries for target-scoped native requirements, - and
[[target]]entries (including nativeinputs,cflags,ldflags, and dynamic linkage fields likeneeded/runpath/soname).
std::build emits TOML in a deterministic, canonical form.
Note: for toolchain-shipped built-in static archives (for example the mbedTLS
archives used by std::tls with the builtin security provider),
[[target]].inputs supports @builtin/<name>.a entries (see
package manifests). Build modules may emit these via
Build.target_add_input(...). On supported native hosts, common built-in
dependency families (libsodium, mbedTLS, libsqlite3, libssh2) are also
auto-linked when the active provider and native .c / .h / .m / .o /
.a inputs require them, so explicit @builtin/... entries are optional for
those common cases.
For package-owned native code that should be linked whenever the package is
imported as a dependency, build modules should emit [[native]] entries via:
Build.add_native(target) -> NativeId(target = ""means no target gate),Build.native_add_input(id, path),Build.native_add_cflag(id, arg),Build.native_add_ldflag(id, arg),Build.native_add_needed(id, soname),Build.native_add_runpath(id, path).
The emitted [[native]] table follows the package-manifest rules: paths are
relative to the owning package root, target is a compiler target triple, and
matching entries are consumed by root package builds and by imported
dependencies. Emit one [[native]] entry per target when a helper is portable
across a specific set of hosted targets, such as linux-x86_64 and
macos-aarch64.
For native header inputs, Build.target_add_input(...) follows the same rule
as direct manifest/CLI builds:
- if the added path ends in
.hand a sibling.cexists, Silk compiles that.c, - otherwise, if a sibling
.mexists, Silk compiles that Objective-C source for supported Apple host-backed Mach-O targets, - otherwise Silk falls back to compiling the header itself as a C translation unit.
Exported API#
Build modules are intended to be normal modules that export a run entrypoint.
Declaring module conformance to std::interfaces::Builder is recommended for
clearer diagnostics and tooling, but the driver requires only that run exists
with the correct signature.
The interface name in module ... as ... is resolved after imports, so build
modules may use the unqualified form (module ... as Builder;) and import
Builder in the import block.
Typical entrypoint:
module hello::build as Builder;
import { Builder } from "std/interfaces";
import build from "std/build";
export async fn run (package_root: string, action: string) -> int {
let ctx: build::Context = build::Context{ package_root: package_root, action: action };
let _ = action;
let _ = ctx;
let mut b: build::Build = build::Build.init();
b.package("hello", "0.1.0");
let t = b.add_executable("hello", "src/main.slk");
b.target_set_output(t, "build/hello");
return b.emit();
}
Notes:
- Build modules may still generate the TOML manifest directly;
std::buildis a convenience layer. - Build modules are allowed to be
asyncso they canawaitduring manifest generation. build::context(argc, argv)remains available for wrapper/legacy usage when you are writing a standalone hosted program and want to parseargvinto aContext.build::run(argc, argv, callback)remains available for older callback-style build modules.
Current manifest-builder methods include:
Build.init() -> Buildpackage(name, version)set_default_target(name)set_security_provider(provider)whereproviderisauto,platform, orbuiltin
Step graph (StepGraph)#
Build modules often need to run deterministic, dependency-ordered “pre-build”
work before emitting the manifest (for example: generating .slk sources,
writing version files, or running small code generators).
std::build provides a small step graph API to make these build-module actions:
- explicit (steps + dependencies),
- deterministic (stable execution order),
- and cacheable (content-addressed generated-file cache).
Concepts:
StepId— an integer handle for a created step.StepKind— the kind of a step (MkdirAll,WriteFile, orRun).- A step graph is executed with
g.run()which runs all steps in dependency order (topological sort) and returns0on success. - Dependencies are declared with
g.depends_on(step, dep)(“rundepbeforestep”). Cycles are rejected.
API surface:
StepGraph.init(package_root)mkdir_all(path, mode) -> StepIdwrite_file(path, bytes, mode) -> StepId(cached)write_file_uncached(path, bytes, mode) -> StepIdwrite_file_string(path, contents, mode) -> StepId(cached)run_cmd(program) -> StepIdcmd_arg(step, arg)cmd_set_cwd(step, cwd)depends_on(step, dep)run() -> int
Path rules:
- Step paths are interpreted relative to
package_rootwhen not absolute.
Caching:
-
WriteFilesteps are cacheable. -
When caching is enabled for a
WriteFilestep: -
the step computes a content hash over the output bytes,
-
stores a blob under
<package_root>/.silk/cache/build/<hash>.blob, -
and only (re)writes the destination file when its bytes differ from the desired output (to avoid unnecessary rebuild churn from timestamp changes).
-
The
silk cachecommand treats these.blobfiles as recognized managed cache entries, so users can inspect/prune/compact them alongside CLI build-cache entries without manually spelunking the cache directory. -
Managed cache cleanup for these blobs is coordinated with normal
silk buildcache activity, so explicit cache maintenance does not race live build-cache reads or writes. -
Runsteps are not cached in the current API.
Minimal example (generate a source file before emitting the manifest):
module app::build as std::interfaces::Builder;
import build from "std/build";
export async fn run (package_root: string, action: string) -> int {
let _ = action;
let mut g: build::StepGraph = build::StepGraph.init(package_root);
let dir = g.mkdir_all("build/gen", 493); // 0755
let gen = g.write_file_string(
"build/gen/generated.slk",
"export fn generated_answer () -> int { return 42; }\n",
420, // 0644
);
g.depends_on(gen, dir);
if g.run() != 0 {
return 1;
}
let mut b: build::Build = build::Build.init();
b.package("app", "0.1.0");
b.sources_add_include("src/**/*.slk");
b.sources_add_include("build/gen/**/*.slk");
let _ = b.add_executable("app", "src/main.slk");
return b.emit();
}
Considerations#
This module is expected to grow toward a Zig-like build system:
- programmatic installation/uninstallation hooks,
- and richer native build configuration beyond the current
cflags/ldflagsfields (additional include path kinds and structured link search paths).
When those features are introduced, they will be specified here first.
Source repository · Edit this page · View Markdown