Merge pull request #2 from shoneyJ/install-tarball

feat: installable toolchain — versioned woc/wovm, self-locating runtime, dist tarball
This commit is contained in:
Shoney Arickathil 2026-08-18 01:24:07 +02:00 committed by GitHub
commit f234839304
11 changed files with 256 additions and 16 deletions

1
.gitignore vendored
View file

@ -100,3 +100,4 @@ prototypes/wo-db/
# un-ignoring their directories above exposed these.
__pycache__/
*.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 --emit <path> -o <out.wob>\n\
usage: woc build <dir> -o <app> [--runtime <path>]\n\
usage: woc version\n\
usage: woc --dump-tokens <path>\n\
usage: woc --dump-ast <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\
\n\
build compiles <dir> like --emit, then produces one self-contained\n\
executable at -o: the wovm runtime binary (--runtime <path>, or\n\
runtime/wovm relative to the current directory when omitted) with the\n\
executable at -o: the wovm runtime binary (--runtime <path>; else\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\
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\
@ -515,7 +517,23 @@ let strip_existing_trailer (rt : string) : string =
then String.sub rt 0 payload_off
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 collector, lookup, image = compile_image path in
@ -528,7 +546,7 @@ let build_mode ~(runtime : string option) (path : string) (out : string) : unit
path;
exit 2
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
Printf.eprintf "woc: runtime binary not found at '%s' -- build it with: make -C runtime wovm\n"
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
(`woc .` inside it). The schema is the one the sample already carried —
top-level `name` (the executable's basename), `version`, `description`,
a `[runtime]` section whose `wo` constraint is accepted and not yet
enforced — plus one new section this feature adds:
a `[runtime]` section whose `wo` constraint is a minimum toolchain
version, enforced against `woc`'s own version — plus one new section
this feature adds:
[build]
runtime = "../runtime/wovm" # optional: wovm to prepend, relative to
@ -629,7 +648,7 @@ let manifest_parse (path : string) : (string * string) list =
let known =
match (!section, key) with
| "", ("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
| _ -> false
in
@ -644,10 +663,38 @@ let manifest_parse (path : string) : (string * string) list =
with End_of_file -> close_in ic);
!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 mf = Filename.concat dir "wo.toml" in
let kvs = manifest_parse mf 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 =
match get "name" with
| 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; "--runtime"; rt |] -> 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 |] ->
if Sys.file_exists path && Sys.is_directory path
&& Sys.file_exists (Filename.concat path "wo.toml")

View file

@ -12,9 +12,11 @@ default: accept
# build the self-contained executable from wo.toml: `woc <dir>` reads the
# manifest and produces target/log-watcher (needs the compiler and runtime
# built once: `just woc-build` and `just wovm-build` at the root)
# built once: `just woc-build` and `just wovm-build` at the root). WO_RUNTIME
# points woc at the repo's freshly built VM — the manifest pins no path, so an
# installed woc self-locates its own wovm instead.
build:
{{ROOT}}/compiler/_build/default/bin/woc .
WO_RUNTIME={{ROOT}}/runtime/wovm {{ROOT}}/compiler/_build/default/bin/woc .
@ls -la target/log-watcher
# the language track's acceptance test: this sample must compile with zero

View file

@ -10,8 +10,7 @@ description = "The Haxe log-watcher ported file-for-file: the systems-track samp
[runtime]
wo = ">= 0.1"
# `woc .` in this directory (or `woc docs/examples/log-watcher` from the
# repo root) builds target/log-watcher from this manifest. Paths resolve
# against this file's directory.
[build]
runtime = "../../../runtime/wovm"
# `woc <dir>` builds target/log-watcher from this manifest. No runtime path is
# pinned here so the project is portable: an installed `woc` finds `wovm` beside
# its own binary. For an in-repo build, point woc at the freshly built VM with
# WO_RUNTIME=<repo>/runtime/wovm — the module justfile's `build` recipe does.

View file

@ -43,6 +43,19 @@ wovm-test:
make -C runtime test-iso
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 acceptance test, the soak): `just log-watcher` runs the acceptance,
# `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
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
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
@ -43,7 +48,7 @@ test-iso: $(ISOBIN)
# the wovm binary (plain optimized build; the test suite is the ASan gate)
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
# need a sanitizer to prove a free actually happened (gc/ cycle fixtures) —

View file

@ -17,6 +17,12 @@
#include "vm.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_db DB; /* the per-shard engine (one shard until iteration 8) */
static wo_wal WAL;
@ -134,7 +140,13 @@ int main(int argc, char **argv) {
}
if (self_rc == 0) {
/* 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) {
fprintf(stderr, "usage: wovm <file.wob> [args...]\n");
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"