RRunes
   ███████ ██  ██████  ██ ██
   ██      ██ ██       ██ ██
   ███████ ██ ██   ███ ██ ██
        ██ ██ ██    ██ ██ ██
   ███████ ██  ██████  ██ ███████

        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):

  1. Reads Sigil.toml, extracts entry and [build] target.
  2. Creates target/ with mkdir(83).
  3. Reads the entry source into memory and compiles it in-process with the embedded compiler (:cmpbuf → the [build] target backend), then writes the image to target/out.

sigil has the whole compiler linked in, so there is no external symc and no fork/execsigil 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.lock yet → for each dep, sigil finds the latest published version in $SIGIL_REGISTRY/<name>/, vendors it, and writes Sigil.lock recording the resolved name = "version".
  • Sigil.lock present → sigil installs the exact locked version of each dep (printed as installed <name>@<ver> [locked]), so installs are reproducible even if a newer version was published. A dependency that is in Sigil.toml but 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 scan requires a Linux desktop environment (xdg-open). On headless systems, compile with sigil build using wasm32 target and open target/scan.html manually.


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.3 exactly 1.2.3
    >=1.2.3 greatest version ≥ 1.2.3
    ~1.2.3 ≥1.2.3 and <1.3.0 (same major.minor)
    ^1.2.3 / bare 1.2.3 ≥1.2.3 and < next incompatible (caret: same major; for 0.x, same minor)

    sigil installs the greatest published version that satisfies the requirement; Sigil.lock still pins resolved versions for reproducibility.

  • Auto-linked at build. sigil build concatenates every vendored runes/<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 scan requires a desktop. xdg-open is hardcoded; headless environments must open target/scan.html manually.
  • sigil add writes "*". It appends offline (no registry lookup), so it can't pin a caret of the current latest the way cargo add does — edit the requirement in Sigil.toml by 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