feat: installable toolchain — version, wovm self-locate, dist tarball

Close the 3 gaps between "builds in the repo" and "installs from a tarball
like Go", so writeonce can ship to other developers.

- VERSION file at repo root single-sources the toolchain version (0.1.0).
- `woc version` -> "writeonce 0.1.0 linux/amd64"; `wovm --version` -> "wovm
  0.1.0" (stamped by the Makefile from VERSION; --version handled only in the
  plain-wovm path so a built app never shadows its own `version` arg).
- wo.toml `[runtime] wo = ">= X.Y"` is now ENFORCED: woc refuses a project
  requiring a newer toolchain than itself (>= and bare version parsed;
  unknown operators accepted forward-compatibly). Was parsed-and-ignored.
- woc self-locates wovm: --runtime > [build] runtime > $WO_RUNTIME > a `wovm`
  beside the woc binary (Sys.executable_name) > runtime/wovm rel CWD. An
  installed woc in <prefix>/bin finds its sibling wovm from any cwd.
- `just dist` (scripts/mkdist.sh) packages writeonce-<ver>-linux-amd64.tar.gz,
  Go-shaped (archive root writeonce/, bin/{woc,wovm}, README, VERSION), with a
  drift guard asserting VERSION == woc == wovm. dist/ gitignored.
- `just install-accept` (scripts/install-accept.sh) is the gate: extract, PATH,
  version, build+run a project from an unrelated cwd, constraint refusal — 6/0.

Verified: install-accept 6/0; woc-test 565/0; wovm suites + cli_smoke;
log-watcher 7/0. Linux-amd64 only (a cross matrix is future work).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-18 00:37:55 +02:00
parent a9e315f100
commit 734ddfe2d6
9 changed files with 248 additions and 9 deletions

1
.gitignore vendored
View file

@ -100,3 +100,4 @@ prototypes/wo-db/
# un-ignoring their directories above exposed these. # un-ignoring their directories above exposed these.
__pycache__/ __pycache__/
*.pyc *.pyc
dist/

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.0

View file

@ -43,6 +43,7 @@ let usage_msg =
usage: woc <dir> (builds when <dir>/wo.toml exists)\n\ usage: woc <dir> (builds when <dir>/wo.toml exists)\n\
usage: woc --emit <path> -o <out.wob>\n\ usage: woc --emit <path> -o <out.wob>\n\
usage: woc build <dir> -o <app> [--runtime <path>]\n\ usage: woc build <dir> -o <app> [--runtime <path>]\n\
usage: woc version\n\
usage: woc --dump-tokens <path>\n\ usage: woc --dump-tokens <path>\n\
usage: woc --dump-ast <path>\n\ usage: woc --dump-ast <path>\n\
usage: woc --dump-owner <path>\n\ usage: woc --dump-owner <path>\n\
@ -91,8 +92,9 @@ let usage_msg =
to compile would be describing bytecode nobody may run.\n\ to compile would be describing bytecode nobody may run.\n\
\n\ \n\
build compiles <dir> like --emit, then produces one self-contained\n\ build compiles <dir> like --emit, then produces one self-contained\n\
executable at -o: the wovm runtime binary (--runtime <path>, or\n\ executable at -o: the wovm runtime binary (--runtime <path>; else\n\
runtime/wovm relative to the current directory when omitted) with the\n\ $WO_RUNTIME, a `wovm` beside this `woc`, or runtime/wovm relative to\n\
the current directory) with the\n\
compiled .wob image and a fixed-size trailer appended, so the result\n\ compiled .wob image and a fixed-size trailer appended, so the result\n\
runs standalone with no separate .wob file or argument (wovm finds the\n\ runs standalone with no separate .wob file or argument (wovm finds the\n\
embedded image via /proc/self/exe -- see docs/plan/oop-vm/00-wob-format.md's\n\ embedded image via /proc/self/exe -- see docs/plan/oop-vm/00-wob-format.md's\n\
@ -515,7 +517,23 @@ let strip_existing_trailer (rt : string) : string =
then String.sub rt 0 payload_off then String.sub rt 0 payload_off
else rt else rt
let default_runtime_path = "runtime/wovm" (* keep in sync with the repo-root VERSION file; `just dist` asserts that
`woc version`, `wovm --version`, and VERSION all agree before packaging. *)
let toolchain_version = "0.1.0"
(* the wovm the standalone build embeds into. When neither --runtime nor the
manifest's [build] runtime is given, resolve the default in order:
1. $WO_RUNTIME -- explicit override
2. a `wovm` beside this `woc` -- the tarball layout <prefix>/bin/{woc,wovm},
located via Sys.executable_name
3. `runtime/wovm` relative to CWD -- the repo/dev fallback *)
let default_runtime_path () : string =
match Sys.getenv_opt "WO_RUNTIME" with
| Some p when p <> "" -> p
| _ ->
let sibling = Filename.concat (Filename.dirname Sys.executable_name) "wovm" in
if Sys.file_exists sibling && not (Sys.is_directory sibling) then sibling
else "runtime/wovm"
let build_mode ~(runtime : string option) (path : string) (out : string) : unit = let build_mode ~(runtime : string option) (path : string) (out : string) : unit =
let collector, lookup, image = compile_image path in let collector, lookup, image = compile_image path in
@ -528,7 +546,7 @@ let build_mode ~(runtime : string option) (path : string) (out : string) : unit
path; path;
exit 2 exit 2
end; end;
let rt_path = match runtime with Some p -> p | None -> default_runtime_path in let rt_path = match runtime with Some p -> p | None -> default_runtime_path () in
if (not (Sys.file_exists rt_path)) || Sys.is_directory rt_path then begin if (not (Sys.file_exists rt_path)) || Sys.is_directory rt_path then begin
Printf.eprintf "woc: runtime binary not found at '%s' -- build it with: make -C runtime wovm\n" Printf.eprintf "woc: runtime binary not found at '%s' -- build it with: make -C runtime wovm\n"
rt_path; rt_path;
@ -569,8 +587,9 @@ let build_mode ~(runtime : string option) (path : string) (out : string) : unit
manifest names the application, so pointing woc at the project is enough manifest names the application, so pointing woc at the project is enough
(`woc .` inside it). The schema is the one the sample already carried — (`woc .` inside it). The schema is the one the sample already carried —
top-level `name` (the executable's basename), `version`, `description`, top-level `name` (the executable's basename), `version`, `description`,
a `[runtime]` section whose `wo` constraint is accepted and not yet a `[runtime]` section whose `wo` constraint is a minimum toolchain
enforced — plus one new section this feature adds: version, enforced against `woc`'s own version — plus one new section
this feature adds:
[build] [build]
runtime = "../runtime/wovm" # optional: wovm to prepend, relative to runtime = "../runtime/wovm" # optional: wovm to prepend, relative to
@ -629,7 +648,7 @@ let manifest_parse (path : string) : (string * string) list =
let known = let known =
match (!section, key) with match (!section, key) with
| "", ("name" | "version" | "description") -> true | "", ("name" | "version" | "description") -> true
| "runtime", "wo" -> true (* accepted, not yet enforced *) | "runtime", "wo" -> true (* minimum toolchain version; enforced in manifest_build *)
| "build", ("runtime" | "target") -> true | "build", ("runtime" | "target") -> true
| _ -> false | _ -> false
in in
@ -644,10 +663,38 @@ let manifest_parse (path : string) : (string * string) list =
with End_of_file -> close_in ic); with End_of_file -> close_in ic);
!kvs !kvs
(* [runtime] wo = ">= X.Y" — a minimum-toolchain-version constraint. Only `>=`
and a bare version are interpreted; any other operator is accepted untouched
(forward-compatible — don't hard-fail on a constraint syntax not grokked
yet). Compared as a (major, minor, patch) triple. *)
let ver_triple (s : string) : int * int * int =
match String.split_on_char '.' s |> List.filter_map int_of_string_opt with
| [ a ] -> (a, 0, 0)
| [ a; b ] -> (a, b, 0)
| a :: b :: c :: _ -> (a, b, c)
| [] -> (0, 0, 0)
let check_runtime_constraint (mf : string) (c : string) : unit =
let c = String.trim c in
let min_req =
if String.length c >= 2 && String.sub c 0 2 = ">=" then
Some (String.trim (String.sub c 2 (String.length c - 2)))
else if String.length c > 0 && c.[0] >= '0' && c.[0] <= '9' then Some c
else None
in
match min_req with
| Some req when compare (ver_triple toolchain_version) (ver_triple req) < 0 ->
Printf.eprintf
"woc: %s: project requires writeonce %s, but this toolchain is %s -- upgrade the toolchain\n"
mf c toolchain_version;
exit 2
| _ -> ()
let manifest_build (dir : string) : unit = let manifest_build (dir : string) : unit =
let mf = Filename.concat dir "wo.toml" in let mf = Filename.concat dir "wo.toml" in
let kvs = manifest_parse mf in let kvs = manifest_parse mf in
let get k = List.assoc_opt k kvs in let get k = List.assoc_opt k kvs in
(match get "runtime.wo" with Some c -> check_runtime_constraint mf c | None -> ());
let name = let name =
match get "name" with match get "name" with
| Some n when n <> "" && not (String.contains n '/') -> n | Some n when n <> "" && not (String.contains n '/') -> n
@ -681,6 +728,9 @@ let () =
| [| _; "build"; path; "-o"; out |] -> build_mode ~runtime:None path out | [| _; "build"; path; "-o"; out |] -> build_mode ~runtime:None path out
| [| _; "build"; path; "-o"; out; "--runtime"; rt |] -> build_mode ~runtime:(Some rt) path out | [| _; "build"; path; "-o"; out; "--runtime"; rt |] -> build_mode ~runtime:(Some rt) path out
| [| _; "build"; path; "--runtime"; rt; "-o"; out |] -> build_mode ~runtime:(Some rt) path out | [| _; "build"; path; "--runtime"; rt; "-o"; out |] -> build_mode ~runtime:(Some rt) path out
| [| _; ("version" | "--version") |] ->
(* Go-style: `writeonce <ver> <os>/<arch>`. linux/amd64 is the only target. *)
Printf.printf "writeonce %s linux/amd64\n" toolchain_version
| [| _; path |] -> | [| _; path |] ->
if Sys.file_exists path && Sys.is_directory path if Sys.file_exists path && Sys.is_directory path
&& Sys.file_exists (Filename.concat path "wo.toml") && Sys.file_exists (Filename.concat path "wo.toml")

View file

@ -43,6 +43,19 @@ wovm-test:
make -C runtime test-iso make -C runtime test-iso
bash runtime/test/cli_smoke.sh bash runtime/test/cli_smoke.sh
# dist: package the toolchain as an installable, Go-style tarball —
# writeonce-<ver>-linux-amd64.tar.gz whose `writeonce/bin/` holds woc + wovm.
# The version is single-sourced from ./VERSION and asserted against both
# binaries (drift guard). See scripts/mkdist.sh.
dist:
./scripts/mkdist.sh
# install-accept: extract the dist tarball to a temp prefix, PATH it, and prove
# `woc version` + a from-scratch project build+run (self-located wovm) + the
# wo-constraint refusal all work — the "tarball install actually works" gate.
install-accept:
./scripts/install-accept.sh
# the sample workload's own recipes live beside it (build from wo.toml, # the sample workload's own recipes live beside it (build from wo.toml,
# the acceptance test, the soak): `just log-watcher` runs the acceptance, # the acceptance test, the soak): `just log-watcher` runs the acceptance,
# `just log-watcher::build` / `::soak 60` the rest — see the module's # `just log-watcher::build` / `::soak 60` the rest — see the module's

View file

@ -2,6 +2,11 @@ CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra -std=c11 CFLAGS ?= -O2 -Wall -Wextra -std=c11
LDFLAGS ?= -pthread LDFLAGS ?= -pthread
# toolchain version, single-sourced from the repo-root VERSION file; stamped
# into wovm's `--version`. `just dist` asserts woc and wovm agree with it.
WO_VERSION := $(shell cat ../VERSION 2>/dev/null || echo 0.0.0-dev)
VERFLAG := -DWO_VERSION='"$(WO_VERSION)"'
wo-rt: wo-rt.c wo-rt: wo-rt.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
@ -43,7 +48,7 @@ test-iso: $(ISOBIN)
# the wovm binary (plain optimized build; the test suite is the ASan gate) # the wovm binary (plain optimized build; the test suite is the ASan gate)
wovm: src/main.c $(VMSRC) $(VMHDR) wovm: src/main.c $(VMSRC) $(VMHDR)
$(CC) $(CFLAGS) -Isrc -I../database/src -o $@ src/main.c $(VMSRC) $(CC) $(CFLAGS) $(VERFLAG) -Isrc -I../database/src -o $@ src/main.c $(VMSRC)
# ASan+UBSan wovm, same flags as the unit tests, for corpus fixtures that # ASan+UBSan wovm, same flags as the unit tests, for corpus fixtures that
# need a sanitizer to prove a free actually happened (gc/ cycle fixtures) — # need a sanitizer to prove a free actually happened (gc/ cycle fixtures) —

View file

@ -17,6 +17,12 @@
#include "vm.h" #include "vm.h"
#include "wal.h" #include "wal.h"
/* stamped by the Makefile from the repo-root VERSION file; the fallback keeps
* a hand-compiled main.c building. `just dist` asserts it matches `woc`. */
#ifndef WO_VERSION
#define WO_VERSION "0.0.0-dev"
#endif
static wo_vm VM; /* 32K value stack: keep it off the C stack */ static wo_vm VM; /* 32K value stack: keep it off the C stack */
static wo_db DB; /* the per-shard engine (one shard until iteration 8) */ static wo_db DB; /* the per-shard engine (one shard until iteration 8) */
static wo_wal WAL; static wo_wal WAL;
@ -134,7 +140,13 @@ int main(int argc, char **argv) {
} }
if (self_rc == 0) { if (self_rc == 0) {
/* no embedded image: the image path is argv[1], and program mode /* no embedded image: the image path is argv[1], and program mode
passes everything after it to the program itself */ passes everything after it to the program itself. `--version` is
handled ONLY here (plain wovm) so a built app never shadows its
own `version` argument. */
if (argc >= 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "version") == 0)) {
printf("wovm %s\n", WO_VERSION);
return 0;
}
if (argc < 2) { if (argc < 2) {
fprintf(stderr, "usage: wovm <file.wob> [args...]\n"); fprintf(stderr, "usage: wovm <file.wob> [args...]\n");
return 2; return 2;

72
scripts/install-accept.sh Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# scripts/install-accept.sh — proves the dist tarball installs and works the way
# Go's does: extract to a prefix, put its bin on PATH, `woc version`, then build
# AND run a throwaway project from an unrelated cwd (so only woc's self-location
# can find wovm), and confirm the wo-version constraint refuses a newer toolchain.
# Run `just dist` first to produce the tarball.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ver="$(cat "$ROOT/VERSION")"
tarball="$ROOT/dist/writeonce-${ver}-linux-amd64.tar.gz"
pass=0
fail=0
ok() { echo "ok $1"; pass=$((pass + 1)); }
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
if [[ ! -f "$tarball" ]]; then
echo "install-accept: $tarball missing -- build it first: just dist" >&2
exit 1
fi
W="$(mktemp -d "${TMPDIR:-/tmp}/wo-install.XXXXXX")"
trap 'rm -rf "$W"' EXIT
# 1. extract exactly like `tar -C /usr/local` — archive root is `writeonce/`
tar -C "$W" -xzf "$tarball"
prefix="$W/writeonce"
if [[ -x "$prefix/bin/woc" && -x "$prefix/bin/wovm" ]]; then
ok "tarball extracts writeonce/bin/{woc,wovm}"
else
bad "extract" "binaries missing under $prefix/bin"
echo; printf 'install-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"; exit 1
fi
export PATH="$prefix/bin:$PATH"
# 2. version reporting (the `go version` equivalent)
got="$(woc version)"
[[ "$got" == "writeonce $ver linux/amd64" ]] && ok "woc version" || bad "woc version" "$got"
got="$(wovm --version)"
[[ "$got" == "wovm $ver" ]] && ok "wovm --version" || bad "wovm --version" "$got"
# 3. build + run a project from an UNRELATED cwd (only self-location finds wovm)
proj="$W/proj"
mkdir -p "$proj"
cat > "$proj/wo.toml" <<EOF
name = "greet"
version = "0.1.0"
[runtime]
wo = ">= 0.1"
EOF
cat > "$proj/main.wo" <<'EOF'
fn main(args: multi Text) -> Int { print("installed writeonce works"); return 0; }
EOF
if ( cd /tmp && woc "$proj" ) >/dev/null 2>&1 && [[ -x "$proj/target/greet" ]]; then
ok "woc builds a project (self-located wovm from an unrelated cwd)"
else
bad "build" "no target/greet produced"
fi
out="$("$proj/target/greet" 2>&1)"
[[ "$out" == "installed writeonce works" ]] && ok "standalone binary runs" || bad "run" "$out"
# 4. the wo constraint refuses a toolchain older than required
sed -i 's/>= 0.1/>= 99.0/' "$proj/wo.toml"
( cd /tmp && woc "$proj" ) >/dev/null 2>&1
[[ $? -eq 2 ]] && ok "wo constraint refuses a newer requirement" || bad "constraint" "did not refuse >= 99.0"
echo
printf 'install-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"
[[ $fail -eq 0 ]]

View file

@ -0,0 +1,42 @@
# writeonce @VER@ (@TRIPLE@)
A single-binary compiled language with an embedded, WAL-durable database. This
archive is the toolchain: `woc` (the compiler) and `wovm` (the runtime VM).
Both are native binaries that depend only on the system C library — nothing
else to install.
## Install (Linux)
Remove any previous install and extract this archive into `/usr/local`,
creating a fresh `/usr/local/writeonce`:
rm -rf /usr/local/writeonce && tar -C /usr/local -xzf writeonce-@VER@-@TRIPLE@.tar.gz
(Run as root, or through `sudo`.)
Add `/usr/local/writeonce/bin` to your `PATH` by adding this line to your
`$HOME/.profile` (or `/etc/profile` for a system-wide install):
export PATH=$PATH:/usr/local/writeonce/bin
Restart your shell, or `source $HOME/.profile` to apply it now. Then verify:
woc version # writeonce @VER@ linux/amd64
wovm --version # wovm @VER@
## Use
woc <project-dir> # builds <project>/target/<name>, one standalone binary
./<project>/target/<name> # run it — no runtime to install on the target host
A writeonce project is a directory with a `wo.toml` manifest and one or more
`.wo` files. `woc` locates `wovm` beside itself, so a tarball install needs no
extra configuration; override with the `$WO_RUNTIME` environment variable or a
`[build] runtime = "..."` key in `wo.toml` if you ever need to.
A `wo.toml` may declare a minimum toolchain version:
[runtime]
wo = ">= @VER@"
`woc` refuses to build a project that requires a newer toolchain than itself.

43
scripts/mkdist.sh Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# scripts/mkdist.sh — package the writeonce toolchain as an installable tarball,
# Go-style. Archive root is `writeonce/` (version-less, like Go's `go/`), so
# `tar -C /usr/local -xzf ...` drops it at /usr/local/writeonce. Builds both
# release binaries, asserts VERSION == `woc version` == `wovm --version` (drift
# guard), stages an install README, and emits dist/<name>.tar.gz + a sha256.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
ver="$(cat VERSION)"
triple="linux-amd64" # the only target today; a cross matrix is future work
name="writeonce-${ver}-${triple}"
# release binaries
dune build --root compiler
make -C runtime wovm >/dev/null
woc="compiler/_build/default/bin/woc"
wovm="runtime/wovm"
# drift guard: the three version sources must agree, else the tarball would ship
# a `wo >= X` constraint nobody can honor
got_woc="$("$woc" version | awk '{print $2}')"
got_vm="$("$wovm" --version | awk '{print $2}')"
[[ "$got_woc" == "$ver" ]] || { echo "mkdist: woc reports $got_woc but VERSION says $ver" >&2; exit 1; }
[[ "$got_vm" == "$ver" ]] || { echo "mkdist: wovm reports $got_vm but VERSION says $ver" >&2; exit 1; }
stage="dist/writeonce"
rm -rf "$stage"
mkdir -p "$stage/bin"
install -m 0755 "$woc" "$stage/bin/woc"
install -m 0755 "$wovm" "$stage/bin/wovm"
cp VERSION "$stage/VERSION"
sed "s/@VER@/$ver/g; s/@TRIPLE@/$triple/g" scripts/install-readme.tmpl.md > "$stage/README.md"
tar -C dist -czf "dist/${name}.tar.gz" writeonce
rm -rf "$stage"
( cd dist && sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256" )
echo "built dist/${name}.tar.gz"
cat "dist/${name}.tar.gz.sha256"