spool
Git-based library dependency manager for Coil.
spool is a Coil userland project (not part of the coil Rust CLI). It resolves
semver tags from git remotes, pins them in coil.lock, fetches into a shared
content-addressed cache, and maintains project-local .spool/deps roots that the
compiler already understands via [module].roots.
Naming#
| Tool | Role |
|---|---|
spool | Library dependencies (install / add / update) and download for direct native shared libraries |
coil package | Embed a .hyc (+ optional native lock metadata) into a runner executable |
Status (M4)#
-
coil.lockread/write - Git fetch into content-addressed cache — bash
fetch.sh+ shared cache - Manage
.spool/deps+ ensure./.spool/depsin[module].roots -
spool installvia./spool install(plan → fetch → link) -
spool add/update - Demo package
greet(examples/greet, also coil-greetv0.1.0) - Consume smoke: install →
use→ compile/run (scripts/smoke_consume.sh) - Private git via host credentials
- Lock integrity + diagnostics
- Transitive deps and diamond errors
- Engine range
[package].coilfail-closed on install/add/update - Hook trust gate:
--ignore-scripts, lockhook_path/hook_hash,allow-include - Current-project
[scripts]runner (--enable-scripts, hostsh) - Dependency
[package].includerunner after link -
spool download/spool install --with-nativesfor direct FFI natives
Requirements#
- Coil toolchain (
coilonPATH, or setCOIL) — needscoil natives dumpfor native downloads - Host
git,sh,curl, andsha256sum - Coil stdlib: default
../coil-stdlib/srcrelative to this repo (coil-stdlib) - coil-toml: default
../coil-toml/srcforcoil.tomldecode (coil-toml)
Native libraries (spool download)#
Direct shared libraries declared in [[ffi.native]] (or embedded in a packaged
exe) are fetched into a content-addressed cache:
~/.coil/natives/cache/<package>/<version>/<sha256_16>/<filename>
Override the root with COIL_NATIVES_DIR. Only direct dload targets are
downloaded; transitive sonames (requires) must come from the OS.
# After coil package ./hello (embeds a native lock):
./spool download ./hello
# Project mode (reads [[ffi.native]] from coil.toml; a lib not there yet is fetched to its `path`, checked against the row's sha256 if it has one):
./spool download
# Install source deps and natives in one step:
./spool install --with-natives
Default spool install does not download natives.
Engine range#
A package may set [package].coil to a semver range against the running Coil
toolchain. The key is optional. Spool stores the string as written.
[package]
name = "http"
version = "1.2.0"
coil = ">=0.1.0"
The engine string is coil --version. coil-lang prints coil 0.1.0 for that
flag and for -V. Spool takes the token after coil and compares it to the
range. It uses the coil binary already selected by COIL or PATH.
spool install, add, and update check the current project, path deps, and
locked git checkouts that already exist. Missing key and in-range >=0.1.0 are
no-ops. Out-of-range >=0.2.0 and ^0.2 fail closed before git fetch.
A git dep whose coil.toml is not on disk yet is checked after checkout, before
link.
The diagnostic names the package, the range, and the running version:
package http requires coil >=0.2.0, running 0.1.0
Range language is the same as git-dep version: caret (^), >=, >, <=,
<, =, exact, or *.
Hook trust#
Hooks are off by default. may_run_hook is the gate. Every host sh of a user
script goes through it first (kind script for current-project [scripts],
kind include for a dependency [package].include). allow_exec is not a
gate.
--enable-scripts opts in. --ignore-scripts always wins, including in CI.
SPOOL_IGNORE_SCRIPTS=0 is the same opt-in the gate already understands.
./spool install --enable-scripts
./spool install --ignore-scripts
./spool allow-include http
spool allow-include <name> records the consumer allowlist in coil.lock,
not in coil.toml. coil-lang still errors on unknown manifest sections, so do
not add a [hooks] table there. [package].include is the include path on a
package. The allowlist is the lock [hooks] table:
[hooks]
allow_include = ['http']
[[package]] stores hook_path and hook_hash for include-hooks. Those
run after link when opted in. They still need allow_include plus a matching
path and hash. Empty hash is first-pin then the same gate. A present hash is
never rewritten. A missing lock row is deny, no sh.
may_run_hook raises these strings:
hooks are off (--ignore-scripts)
include-hook for http is not allowlisted
untrusted hook: missing lock hash for http
hook path mismatch for http
hook hash mismatch for http
Project scripts#
The current project’s coil.toml may declare lifecycle scripts. Paths are
relative to the project root. Missing keys are no-ops. Unknown keys are errors
(coil-lang already owns that schema).
[scripts]
pre_install = "./scripts/pre-install.sh"
post_install = "./scripts/post-install.sh"
pre_update = "./scripts/pre-update.sh"
post_update = "./scripts/post-update.sh"
Default is still off. --enable-scripts opts in on install, add, and
update. --ignore-scripts wins even when both flags are present.
With --enable-scripts:
spool installrunspre_installthen fetch/link, thenpost_installspool updaterunspre_update/post_updatespool adduses the install pair
pre_* runs after engine checks and before fetch/link. post_* runs after a
successful link. sh runs from the project root. Non-zero exit is
spool: <path> exited <status> and aborts. A missing file is
spool: missing script <path>.
Every sh goes through may_run_hook first (kind script). Scripts skip
the include allowlist. They still need a lock hash.
Hashes live in lock [scripts], not on a [[package]] row. The hash is
git hash-object of the file. First opted-in run records path and hash.
After that, the existing pin is checked first. A changed file is a hash
mismatch. It does not sh and does not rewrite the lock:
[scripts]
pre_install = './scripts/pre-install.sh'
pre_install_hash = 'abc123'
post_install = './scripts/post-install.sh'
post_install_hash = 'def456'
That diagnostic is hook hash mismatch for <package>, using the current
[package].name (or app if the name is empty).
A dependency’s [scripts] are not executed during a consumer install. Those
fire only when that repo is the current project.
Include hooks#
A library may declare a hook that runs when another project depends on it.
[package]
name = "native-bits"
include = "./hooks/include.sh"
The path is relative to that package’s checkout, not the consumer. Missing
include is a no-op. A declared file that is not on disk is
spool: missing include-hook <name> <path>.
spool install, add, and update run include-hooks after link, including
transitives. sh runs from the checkout. SPOOL_PROJECT is still the
consumer.
Default is still off. --enable-scripts / SPOOL_IGNORE_SCRIPTS=0 opt in.
--ignore-scripts wins even when both flags are present. Include-hooks also
need spool allow-include <name> on the consumer. Opt-in without that
allowlist is deny, no sh. allow_exec is not the gate.
Every include sh goes through may_run_hook first (kind include). The
pin is hook_path / hook_hash on that dep’s [[package]] row. The hash is
git hash-object of the file. First opted-in run records them when that row
exists and hook_hash is empty. No [[package]] row (a path dep, or any
name not in the lock) is missing lock hash: no first-pin, no sh. After a
pin exists, it is checked first. A changed include file is a hash mismatch.
It does not sh and does not rewrite the lock:
[[package]]
name = 'http'
hook_path = './hooks/include.sh'
hook_hash = 'abc123'
Non-zero exit aborts the consumer command:
spool: include-hook http ./hooks/include.sh exited 9
Install order#
spool install is this sequence. Gates live in Hook trust,
Project scripts, and Include hooks.
check_install—coil.tomlpresent; git deps already incoil.lockcheck_engineon the current project, path deps, and cachedcoil.toml— engine first so an unsatisfied[package].coilnever fetchespre_installif--enable-scripts— before fetch so a failing project script leaves lock and link alone- Plan: write
.spool/fetch.shand.spool/links.tsv - Bash
fetch.sh: clone/fetch + worktree - Verify lock
content_hashagainst each checkoutHEADtree check_engineagain on new checkouts- Link
.spool/depsand inject[module].roots - Include-hooks after link (the hook can see its own checkout)
post_installif--enable-scripts— only after a successful link; does not run if include failed
Hooks default off. --enable-scripts opts in. --ignore-scripts always wins.
Every sh goes through may_run_hook first.
Two lock homes: consumer [scripts] hashes live in lock [scripts]
(git hash-object; the current project is not a [[package]] row). Include
pins are hook_path / hook_hash on that dep’s [[package]]. First opted-in
run pins. An existing lock hash is checked first; a changed file is a mismatch
and does not sh or rewrite the lock. No lock row (path deps) is deny, no
sh. Include-hooks also need spool allow-include <name>. A dep’s own
[scripts] never run on a consumer install.
add uses the install pair. update uses pre_update / post_update. Both
share the same materialize, engine first, include/scripts at the end.
This path does not fetch a .so or write [ffi] search_paths.
Cache#
Default root: $XDG_CACHE_HOME/coil or ~/.cache/coil.
Override:
- Env:
COIL_CACHE_DIR(wins) - File:
~/.config/coil/config.toml→[cache] dir = "…"
Layout:
<cache_root>/git/
<host>/<owner>/<repo>/ # bare clone
checkouts/
<tree-id>/ # detached worktree
Develop#
export COIL=/path/to/coil-lang/target/release/coil
$COIL test
./spool help
./scripts/smoke_install.sh # local git fixture → install
./scripts/smoke_add.sh # add git+path deps, then update
./scripts/smoke_consume.sh # add greet, `use greet::hello`, run it
./scripts/smoke_integrity.sh # hash mismatch, missing/corrupt lock
./scripts/smoke_auth.sh # git auth failure message
./scripts/smoke_transitive.sh # unify compatible pins, diamond error
./scripts/smoke_engine.sh # [package].coil range: omit / in-range / too-old
./scripts/smoke_hooks.sh # --ignore-scripts, allow-include, default include-hooks stay off
./scripts/smoke_scripts.sh # current-project [scripts]: default off, opt-in, fail, no dep scripts
./scripts/smoke_include.sh # dep include-hooks: allowlist, hash pin, fail, no dep [scripts]
Private git#
spool does not store credentials. It runs host git with GIT_TERMINAL_PROMPT=0 so a missing credential fails instead of hanging on a prompt.
Use whatever already works for git clone on your machine:
ssh-agentandgit@host:owner/repo.gitURLsGIT_ASKPASS/SSH_ASKPASS- git credential helpers
url.<base>.insteadOfin~/.gitconfigto rewrite HTTPS to SSH
GIT_SSH_COMMAND and those variables are passed through to clone/fetch/ls-remote.
Consume a library#
.spool/deps/<name> is a symlink to the checkout’s src/ directory when that
exists, so use greet::hello maps to hello.hy in the package.
./spool add greet --git https://github.com/ardax-corp/coil-greet.git --version '^0.1'
use greet::hello;
greet is function-style on purpose. Userland class types still cannot cross
module boundaries. Enums and functions in a git/path dep compile and run.
The locked install order is under Install order.
add / update resolve tags and merge coil.lock, then the same materialize.
Transitive git deps unify compatible pins and error on diamonds. add uses
pre_install / post_install. update uses pre_update / post_update.
Coil quirks this repo works around#
- No forward references within a module file
env::exec/env::exitwarnings fail in-memory compile;externin imported modules panics (invalid library handle);system(3)breaks afterVecalloc- So git stays in the bash driver; Coil does plan/pick/lock/link only