diff --git a/.gitignore b/.gitignore
index e5975fb..762749c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,3 +100,4 @@ prototypes/wo-db/
# un-ignoring their directories above exposed these.
__pycache__/
*.pyc
+dist/
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/compiler/bin/main.ml b/compiler/bin/main.ml
index a3240bb..fea8970 100644
--- a/compiler/bin/main.ml
+++ b/compiler/bin/main.ml
@@ -43,6 +43,7 @@ let usage_msg =
usage: woc
(builds when /wo.toml exists)\n\
usage: woc --emit -o \n\
usage: woc build -o [--runtime ]\n\
+ usage: woc version\n\
usage: woc --dump-tokens \n\
usage: woc --dump-ast \n\
usage: woc --dump-owner \n\
@@ -91,8 +92,9 @@ let usage_msg =
to compile would be describing bytecode nobody may run.\n\
\n\
build compiles like --emit, then produces one self-contained\n\
- executable at -o: the wovm runtime binary (--runtime , or\n\
- runtime/wovm relative to the current directory when omitted) with the\n\
+ executable at -o: the wovm runtime binary (--runtime ; 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 /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 /`. 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")
diff --git a/justfile b/justfile
index 5f587ff..69a8542 100644
--- a/justfile
+++ b/justfile
@@ -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--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
diff --git a/runtime/Makefile b/runtime/Makefile
index bb5744c..a4f3411 100644
--- a/runtime/Makefile
+++ b/runtime/Makefile
@@ -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) —
diff --git a/runtime/src/main.c b/runtime/src/main.c
index 46c78d6..674b7ee 100644
--- a/runtime/src/main.c
+++ b/runtime/src/main.c
@@ -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 [args...]\n");
return 2;
diff --git a/scripts/install-accept.sh b/scripts/install-accept.sh
new file mode 100755
index 0000000..3642c03
--- /dev/null
+++ b/scripts/install-accept.sh
@@ -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" <= 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 ]]
diff --git a/scripts/install-readme.tmpl.md b/scripts/install-readme.tmpl.md
new file mode 100644
index 0000000..c17a200
--- /dev/null
+++ b/scripts/install-readme.tmpl.md
@@ -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 # builds /target/, one standalone binary
+ .//target/ # 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.
diff --git a/scripts/mkdist.sh b/scripts/mkdist.sh
new file mode 100755
index 0000000..5458c90
--- /dev/null
+++ b/scripts/mkdist.sh
@@ -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/.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"