5.7 KiB
Build & Distribution
How Skald is built for local development and how a single portable binary is produced for headless servers (Ubuntu Server, AWS containers, mini-PCs).
TLS / crypto: rustls + ring, no OpenSSL
Skald links no OpenSSL and no aws-lc-rs. All HTTPS/WSS traffic goes through
rustls with the pure-ring crypto backend.
This is the single most important property for a portable binary: there is no
dynamic link to a system libssl/libcrypto, so the binary does not depend on
the OpenSSL version installed (or missing) on the target machine.
How it is wired:
-
Every
reqwestdependency in the workspace is declareddefault-features = false, features = ["rustls-no-provider", …].reqwest 0.13has no bundled-ringfeature, onlyrustls-no-provider(rustls with no crypto provider selected). -
Because no provider is bundled, exactly one process-wide provider is installed at startup, before any TLS handshake, in
src/main.rs:rustls::crypto::ring::default_provider() .install_default() .expect("install rustls ring crypto provider"); -
rustlsis pinned as a direct dependency of the root crate only to select the provider:default-features = false, features = ["ring", "std", "tls12", "logging"]. Without this, rustls' default feature would pullaws-lc-rsback in. -
teloxide(Telegram plugin) is ondefault-features = false, features = ["rustls", …]so it does not drag innative-tls/OpenSSL either.
Feature-unification trap. rustls is a single shared crate across every consumer. If any dependency enables its
aws_lc_rsfeature,aws-lc-rs(a cmake/NASM C build) comes back for the whole tree. Consumers that must be kept onring: allreqwest(done),tokio-tungstenite(relay client — it declarestokio-rustlswithdefault-features = false, so it inheritsring), and the embedded Tailscale provider (see below). Verify with:cargo tree -e no-dev -i aws-lc-rs # must print "did not match any packages" cargo tree -e no-dev -i ring # must list rustls consumers
Native (development) build
cargo build # or: cargo run
./run.sh # supervisor loop (rebuilds on exit -1); -d for debug
On macOS/dev machines this dynamically links the system libc, which is fine — the portability concern only applies to the distributed binary.
Portable static build (musl)
The distribution target is x86_64-unknown-linux-musl (or
aarch64-unknown-linux-musl), which produces a fully static binary: no
libssl, no libc, no shared libraries at all. Copy the single file to the
server and run it.
Since there is no OpenSSL/aws-lc build, the only native code left to
cross-compile is SQLite (bundled via libsqlite3-sys), the tree-sitter C
grammars, and ring — all of which the musl-cross toolchain handles out of the
box. There is no host toolchain requirement other than Docker:
scripts/build-musl.sh # x86_64 static binary
TARGET=aarch64-unknown-linux-musl \
IMAGE=messense/rust-musl-cross:aarch64-musl \
scripts/build-musl.sh # arm64 static binary
Output: target/musl/<target>/release/skald. Verify it is static:
file target/musl/x86_64-unknown-linux-musl/release/skald
# → ELF 64-bit LSB executable, x86-64, statically linked, …
The script builds --no-default-features (see the feature table below) to drop
whisper-local; set FEATURES="" to include it (needs a C++ cross-compile).
glibc alternative
If a static musl binary is more than you need, a glibc binary built inside an
old base image (e.g. debian:bullseye / ubuntu:22.04) runs on any server with
a same-or-newer glibc. Now that OpenSSL is gone, this build needs no special
crypto handling — the normal gnu toolchain compiles SQLite/ring/tree-sitter
directly. It is not fully static (glibc stays dynamic) but is broadly compatible
and simpler to produce than musl.
Cargo features that affect the binary
| Feature | Default | Effect | Portability cost |
|---|---|---|---|
whisper-local |
on | Local STT via whisper.cpp | Compiles whisper.cpp (C++) — heavy; drop for server builds (--no-default-features) |
embedded-tailscale |
off | Pure-Rust embedded Tailscale provider (no system tailscaled) |
Pulls the tailscale crate, which forces aws-lc-rs (cmake/NASM C build) back into the tree — breaks the ring-only static binary |
The recommended Tailscale provider, tailscale_sys (drives the system
tailscaled), is always compiled and needs neither feature. embedded-tailscale
exists only for a self-contained mesh where installing tailscaled is not an
option; enabling it re-introduces the aws-lc-rs C build. See
plugins/remote.md.
What the binary needs at runtime
- Nothing dynamically linked in the musl build — no OpenSSL, no libc.
- SQLite is statically bundled (compiled from source), so the target needs no
system
libsqlite3. - Python is optional and only required for Python-based MCP servers; the app
starts without it (see
run.sh). - The web UI is static assets under
web/(web.static_dir); serve it from the same binary or point a reverse proxy at it.
Self-restart on a server
run.sh is a dev supervisor: exit 255 → rebuild+restart, exit 0 → stop. On a
server, prefer platform-native supervision instead of the shell loop:
systemd with Restart=on-failure (map the restart tool's exit(-1) = 255 to
a restart), a container restart policy, or launchd on macOS. See
self-rewriting.md.