From 734ddfe2d64097582a771db3b4e7ee5ea8d41999 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 18 Aug 2026 00:37:55 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20installable=20toolchain=20=E2=80=94=20v?= =?UTF-8?q?ersion,=20wovm=20self-locate,=20dist=20tarball?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /bin finds its sibling wovm from any cwd. - `just dist` (scripts/mkdist.sh) packages writeonce--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) --- .gitignore | 1 + VERSION | 1 + compiler/bin/main.ml | 64 ++++++++++++++++++++++++++---- justfile | 13 ++++++ runtime/Makefile | 7 +++- runtime/src/main.c | 14 ++++++- scripts/install-accept.sh | 72 ++++++++++++++++++++++++++++++++++ scripts/install-readme.tmpl.md | 42 ++++++++++++++++++++ scripts/mkdist.sh | 43 ++++++++++++++++++++ 9 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 VERSION create mode 100755 scripts/install-accept.sh create mode 100644 scripts/install-readme.tmpl.md create mode 100755 scripts/mkdist.sh 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" < "$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"