███████ ██ ██████ ██ ██
██ ██ ██ ██ ██
███████ ██ ██ ███ ██ ██
██ ██ ██ ██ ██ ██
███████ ██ ██████ ██ ███████
the Symbolic package manager
Sigil is the cargo-shaped tool around symc. It scaffolds runes
(Symbolic packages), builds them for any target, resolves dependencies from an
on-disk registry, and sandbox-scans them before publish.
Sigil is itself a rune — written in Symbolic
(sigil/src/main.sym, 445 lines), compiled by
symc, and shipped alongside it by install.sh. Every feature described in
this document comes directly from that source file.
Setup
install.sh builds and installs sigil automatically:
bash install.sh
source ~/.symbolic/env
This sets three environment variables sigil reads at runtime:
| Variable | Purpose |
|---|---|
SIGIL_CC |
Path to a symc binary — only sigil scan/publish need it (they compile to wasm). build/run/test use the embedded compiler. |
SIGIL_REGISTRY |
Path to the on-disk package registry directory |
SIGIL_HARNESS |
Path to scan.html (WASI sandbox harness used by sigil scan) |
SIGIL_REGISTRY must point to a directory you control — a local path or a git
checkout of a shared registry. The other two are set automatically by
install.sh.
The Sigil.toml manifest
Every rune has a Sigil.toml at its root. sigil new creates one; the format
used by sigil is:
[package]
name = "hello"
version = "0.1.0"
entry = "src/main.sym"
[build]
target = "x64-linux"
[dependencies]
sha256 = "*"
| Field | Required | Meaning |
|---|---|---|
[package] name |
yes | rune name (used by sigil publish) |
[package] version |
yes (defaults to "0.1.0" on publish) |
published version string |
[package] entry |
yes | source file passed to symc |
[build] target |
no | symc --target value; omit for x64-linux |
[dependencies] <name> = "*" |
no | dependency from registry |
Sigil parses the manifest with simple substring search (:tval in sigil)
— it reads quoted TOML values for known keys. The "*" version selector means
"latest version in the registry."
Commands
sigil new <name>
Scaffolds a new rune at ./<name>/:
sigil new myapp
Creates:
myapp/
Sigil.toml ← [package] name/version/entry + [build] target=x64-linux
src/main.sym ← ((hello from myapp\n)) > @screen \n !!
No compiler is needed — this command only creates files.
sigil build
Compiles the rune's entry source to target/out:
sigil build
# sigil: built target/out
Implementation (:dobld):
- Reads
Sigil.toml, extractsentryand[build] target. - Creates
target/withmkdir(83). - Reads the
entrysource into memory and compiles it in-process with the embedded compiler (:cmpbuf→ the[build] targetbackend), then writes the image totarget/out.
sigil has the whole compiler linked in, so there is no external symc and no
fork/exec — sigil build works identically on every target, including
inside a WASI sandbox (wasmtime run --dir . sigil.wasm build). The output
format follows [build] target: a native ELF, wasm binary, PE, etc.
sigil run
Builds the rune then execves target/out, replacing the sigil process:
sigil run
printf 'input' | sigil run # stdin is forwarded
Because sigil execves rather than fork+execs, the launched binary
inherits the full process environment, stdin, stdout, and stderr unchanged.
sigil test
Builds the rune and runs it in a child process. Prints PASSED and exits 0 if
the binary exits 0; prints FAILED and exits 1 otherwise:
sigil test
# sigil: test PASSED
# sigil: test FAILED
There is no separate test file — the rune's own entry point is its test. A rune passes if its binary exits 0.
sigil clean
Removes target/out (via unlink(87)) and target/ (via rmdir(84)):
sigil clean
# sigil: cleaned target
sigil add <name>
Appends a dependency line to Sigil.toml. Creates the [dependencies] table
if it does not exist:
sigil add sha256
Opens Sigil.toml with O_WRONLY|O_APPEND and writes:
[dependencies]
sha256 = "*"
(or just the sha256 = "*" line if [dependencies] already exists). No
network request is made.
sigil install
Reads every [dependencies] entry in Sigil.toml and vendors each one into
./runes/<name>/, honoring Sigil.lock:
- No
Sigil.lockyet → for each dep, sigil finds the latest published version in$SIGIL_REGISTRY/<name>/, vendors it, and writesSigil.lockrecording the resolvedname = "version". Sigil.lockpresent → sigil installs the exact locked version of each dep (printed asinstalled <name>@<ver> [locked]), so installs are reproducible even if a newer version was published. A dependency that is inSigil.tomlbut not yet in the lock is resolved to latest as above.
sigil install
# sigil: installed sha256@0.2.0
# sigil: wrote Sigil.lock
Version selection (when resolving latest): sigil walks the <name>/
directory inside the registry, compares subdirectory names byte-lexicographically
(:strgt), and picks the greatest — 0.2.0 > 0.1.0 works because digit strings
sort correctly lexicographically. If a dependency is not found in the registry,
a warning is printed and it is skipped.
Result: the dependency lands at ./runes/<name>/ as a vendored copy, pinned
in Sigil.lock.
sigil update
Drops Sigil.lock, re-resolves the latest published version of every
dependency, vendors them, and rewrites Sigil.lock with the new versions:
sigil update
# sigil: installed sha256@0.3.0
# sigil: wrote Sigil.lock
This is the only command that moves a pinned dependency to a newer version.
Sigil.lock
A generated, human-readable lockfile that pins each resolved dependency to an
exact version, so a fresh sigil install reproduces the same runes/ tree:
# Sigil.lock - generated by sigil; do not edit by hand.
sha256 = "0.2.0"
std = "0.1.0"
Commit it alongside Sigil.toml. sigil install reads it (pin); sigil update
regenerates it (bump). It is intentionally flat (name = "version") so the same
tiny TOML reader the rest of sigil uses can parse it with no extra machinery.
sigil publish
Auto-scans the rune for malicious behavior, then publishes it to the registry.
Requires SIGIL_CC and SIGIL_REGISTRY:
sigil publish
# sigil: scan clean — sandboxed (only WASI stdio/clock/random)
# sigil: published sha256@0.2.0
Step 1 — auto-scan (:scwc):
Compiles the entry source to target/scan.wasm via $SIGIL_CC --target wasm32.
Step 2 — capability check (:wimports):
Parses the wasm binary's import section (LEB128 decode). Every import is
checked against the allowlist of five WASI calls:
fd_write fd_read random_get clock_time_get proc_exit
Any import outside this list causes SCAN FAILED and publish is aborted.
Step 3 — vendor:
Creates $SIGIL_REGISTRY/<name>/<version>/ and copies Sigil.toml + the full
src/ tree there with cpf/cptree.
To publish a new version, bump version in Sigil.toml first. Sigil does not
prevent overwriting an existing version.
sigil scan
Compiles the rune to wasm, embeds the result in the sandbox harness, and opens it in a browser for interactive inspection:
sigil scan
# sigil: scan -> target/scan.html (sandboxed wasm; opening in browser)
Step 1 (:scwc): Compiles entry to target/scan.wasm using
$SIGIL_CC --target wasm32.
Step 2 (:scgn): Reads $SIGIL_HARNESS (scan.html), finds the
@@WASM@@ marker, and splices in the wasm binary base64-encoded (RFC 4648,
implemented in pure Symbolic). Writes the result to target/scan.html.
Step 3: Launches target/scan.html with /usr/bin/xdg-open.
The scan.html harness (sigil/scan.html) runs the wasm module in a
browser WASI sandbox. The sandbox grants only the five allowed imports
above. Every host call is logged in the browser; any attempt to reach outside
the allowlist is flagged visibly. This lets you vet a third-party rune for
filesystem access, network calls, or other capabilities before running it
natively.
Note:
sigil scanrequires a Linux desktop environment (xdg-open). On headless systems, compile withsigil buildusingwasm32target and opentarget/scan.htmlmanually.
sigil help
Prints the command summary and exits 0:
sigil help
sigil - the Symbolic package manager (cargo for runes)
sigil new <name> scaffold a new rune
sigil build compile this rune to target/out
sigil run build, then run it
sigil clean remove target/
sigil test build the rune and run it; pass iff it exits 0
sigil add <name> add a dependency to Sigil.toml
sigil install vendor [dependencies] from the registry into runes/
sigil update re-vendor the latest version of each dependency
sigil publish auto-scan, then publish this rune to the registry
sigil scan compile to wasm and run sandboxed in a browser
sigil help this message
compiler is found via SIGIL_CC (one symc; build target comes from Sigil.toml [build] target).
the registry is SIGIL_REGISTRY; scan also needs SIGIL_HARNESS (the scan.html sandbox harness).
The registry
A registry is a plain directory tree. Because it is just files, a git
repository is a valid registry: git push to share it; git clone + set
SIGIL_REGISTRY to the checkout to consume it. No HTTP, no TLS, no central
server.
Layout:
$SIGIL_REGISTRY/
sha256/
0.1.0/
Sigil.toml
src/
main.sym
0.2.0/
Sigil.toml
src/
main.sym
Publish: sigil publish creates the <name>/<version>/ directory and
copies Sigil.toml + src/ there.
Install/update: sigil reads the <name>/ directory via getdents64(217),
picks the lexicographically greatest version subdirectory, and copies the tree
to ./runes/<name>/ with cptree.
Vendored dependencies
After sigil install, dependencies live in ./runes/:
my-rune/
Sigil.toml
src/
main.sym
runes/
sha256/
Sigil.toml
src/
main.sym ← vendored source
sigil build auto-links these at compile time: it concatenates each
runes/<name>/src/main.sym (definitions only — the standalone !! terminator
is stripped) ahead of your entry, so a dependency's functions are callable with
no manual include. It's the same model the compiler uses for itself, where
build-symc.sh concatenates std + lex + parse + ir + back before symc.
Dependency resolution (cargo-parity)
sigil install resolves the full dependency graph, not just direct deps:
Transitive. After vendoring a dependency, sigil reads its
Sigil.toml[dependencies]and installs those too, recursively. A run-scoped visited set dedups shared deps (vendored once) and breaks dependency cycles.Version requirements. Requirements are matched with a real numeric semver comparator (so
0.10.0 > 0.9.0, which byte-lexicographic compare gets wrong):Requirement Matches *any version (latest) =1.2.3exactly 1.2.3>=1.2.3greatest version ≥ 1.2.3~1.2.3≥1.2.3and<1.3.0(same major.minor)^1.2.3/ bare1.2.3≥1.2.3and<next incompatible (caret: same major; for0.x, same minor)sigil installs the greatest published version that satisfies the requirement;
Sigil.lockstill pins resolved versions for reproducibility.Auto-linked at build.
sigil buildconcatenates every vendoredrunes/<name>/src/main.sym(definitions; the standalone!!terminator is stripped, never the!!>break token) ahead of the rune's own entry before compiling — so a dependency's functions are available with no manual include.
Known limitations
sigil scanrequires a desktop.xdg-openis hardcoded; headless environments must opentarget/scan.htmlmanually.sigil addwrites"*". It appends offline (no registry lookup), so it can't pin a caret of the current latest the waycargo adddoes — edit the requirement inSigil.tomlby hand to use^/~/=/>=.
End-to-end example
# install the toolchain
bash install.sh && source ~/.symbolic/env
# set up a registry
mkdir -p ~/sigil-registry
export SIGIL_REGISTRY=~/sigil-registry
# publish the sha256 rune (from this repo)
cd sha256
sigil publish # scan + vendor into registry
cd ..
# create a new rune that uses sha256
sigil new myapp
cd myapp
sigil add sha256 # append to Sigil.toml
sigil install # vendor sha256 into runes/sha256/
sigil build # compiles src/main.sym -> target/out
sigil run # builds if needed, then runs