feat: woc <dir> builds from wo.toml

- a directory carrying wo.toml is a PROJECT: `woc .` inside it (or
  `woc path/to/project` from anywhere) reads the manifest and produces
  <target>/<name>, exactly what `woc build <dir> -o ...` produces
- schema is the one the sample already carried -- top-level name/
  version/description, [runtime] wo (accepted, not yet enforced) --
  plus a new [build] section: runtime (wovm to prepend) and target
  (output dir, default "target"), both relative to the manifest's own
  directory so the build is invocation-point independent
- unknown keys and sections are hard errors: a typo'd key silently
  ignored would build the wrong thing
- directories WITHOUT wo.toml keep check-only semantics -- the corpus
  is full of those; oop-e2e 71/0, woc-test 565/0, log-watcher 7/0
- sample's wo.toml gains the [build] section; target/ gitignored

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-15 00:55:29 +02:00
parent e1f31cf75d
commit 348564d620
3 changed files with 114 additions and 2 deletions

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
# Cargo build artifacts # Cargo build artifacts
/target /target
# `woc .` manifest builds (wo.toml [build] target)
/docs/examples/log-watcher/target
# Rust runtime (crates/rt/): compiled binary + build artifacts # Rust runtime (crates/rt/): compiled binary + build artifacts
/crates/rt/target /crates/rt/target
/crates/rt/Cargo.lock /crates/rt/Cargo.lock

View file

@ -40,6 +40,7 @@
let usage_msg = let usage_msg =
"usage: woc <path>\n\ "usage: woc <path>\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 --dump-tokens <path>\n\ usage: woc --dump-tokens <path>\n\
@ -56,7 +57,11 @@ let usage_msg =
\n\ \n\
With no flag, <path> is fully compiled (lexed, parsed, typechecked,\n\ With no flag, <path> is fully compiled (lexed, parsed, typechecked,\n\
ownership-checked) and nothing is printed on success; diagnostics,\n\ ownership-checked) and nothing is printed on success; diagnostics,\n\
if any, print to stderr.\n\ if any, print to stderr. One exception: a directory containing a\n\
wo.toml manifest is BUILT instead — `woc .` inside a project (or\n\
`woc path/to/project` from anywhere) reads the manifest's `name` plus\n\
the optional [build] runtime/target keys (paths relative to the\n\
manifest) and produces <target>/<name> exactly as `woc build` would.\n\
\n\ \n\
--dump-tokens prints one line per lexed token to stdout, in source\n\ --dump-tokens prints one line per lexed token to stdout, in source\n\
order (\"LINE:COL KIND\" or \"LINE:COL KIND(payload)\"), ending with\n\ order (\"LINE:COL KIND\" or \"LINE:COL KIND(payload)\"), ending with\n\
@ -559,6 +564,100 @@ let build_mode ~(runtime : string option) (path : string) (out : string) : unit
finish collector lookup finish collector lookup
end end
(* ---- manifest build ---------------------------------------------------
`woc <dir>` where <dir>/wo.toml exists is a BUILD, not a check: the
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:
[build]
runtime = "../runtime/wovm" # optional: wovm to prepend, relative to
# the manifest's own directory (default:
# `runtime/wovm` relative to the CWD, the
# same as `woc build` with no --runtime)
target = "target" # optional: output directory, relative to
# the manifest's directory
Anything else is an error: a typo'd key silently ignored would build the
wrong thing. A directory WITHOUT wo.toml keeps today's meaning (check
only), and the corpus is full of those. *)
let manifest_parse (path : string) : (string * string) list =
let fail line msg =
Printf.eprintf "woc: %s:%d: %s\n" path line msg;
exit 2
in
let ic = try open_in path with Sys_error m -> Printf.eprintf "woc: %s\n" m; exit 2 in
let kvs = ref [] in
let section = ref "" in
let lineno = ref 0 in
(try
while true do
let raw = input_line ic in
incr lineno;
let line = String.trim raw in
if line = "" || (String.length line >= 1 && line.[0] = '#') then ()
else if line.[0] = '[' then begin
if line.[String.length line - 1] <> ']' then fail !lineno "malformed section header";
section := String.sub line 1 (String.length line - 2);
if !section <> "runtime" && !section <> "build" then
fail !lineno (Printf.sprintf "unknown section [%s] (runtime and build exist)" !section)
end
else
match String.index_opt line '=' with
| None -> fail !lineno "expected `key = \"value\"`"
| Some eq ->
let key = String.trim (String.sub line 0 eq) in
let v = String.trim (String.sub line (eq + 1) (String.length line - eq - 1)) in
if String.length v < 2 || v.[0] <> '"' || v.[String.length v - 1] <> '"' then
fail !lineno (Printf.sprintf "`%s`: only quoted string values are supported" key);
let v = String.sub v 1 (String.length v - 2) in
let known =
match (!section, key) with
| "", ("name" | "version" | "description") -> true
| "runtime", "wo" -> true (* accepted, not yet enforced *)
| "build", ("runtime" | "target") -> true
| _ -> false
in
if not known then
fail !lineno
(if !section = "" then
Printf.sprintf "unknown key `%s` (name, version, description exist)" key
else
Printf.sprintf "unknown key `%s` in [%s]" key !section);
kvs := ((if !section = "" then key else !section ^ "." ^ key), v) :: !kvs
done
with End_of_file -> close_in ic);
!kvs
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
let name =
match get "name" with
| Some n when n <> "" && not (String.contains n '/') -> n
| Some _ ->
Printf.eprintf "woc: %s: `name` must be a bare file name\n" mf;
exit 2
| None ->
Printf.eprintf "woc: %s: `name` is required\n" mf;
exit 2
in
(* paths in the manifest are the PROJECT's, so they resolve against the
manifest's directory — `woc .` inside the project and `woc path/to/it`
from anywhere must build the same thing *)
let resolve rel = if Filename.is_relative rel then Filename.concat dir rel else rel in
let runtime = Option.map resolve (get "build.runtime") in
let target = resolve (match get "build.target" with Some t when t <> "" -> t | _ -> "target") in
(if not (Sys.file_exists target) then
try Sys.mkdir target 0o755
with Sys_error m ->
Printf.eprintf "woc: %s\n" m;
exit 2);
build_mode ~runtime dir (Filename.concat target name)
let () = let () =
match Sys.argv with match Sys.argv with
| [| _; "--dump-tokens"; path |] -> dump_tokens path | [| _; "--dump-tokens"; path |] -> dump_tokens path
@ -569,7 +668,11 @@ 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
| [| _; path |] -> check_only path | [| _; path |] ->
if Sys.file_exists path && Sys.is_directory path
&& Sys.file_exists (Filename.concat path "wo.toml")
then manifest_build path
else check_only path
| _ -> | _ ->
prerr_string usage_msg; prerr_string usage_msg;
exit 2 exit 2

View file

@ -9,3 +9,9 @@ description = "The Haxe log-watcher ported file-for-file: the systems-track samp
[runtime] [runtime]
wo = ">= 0.1" 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"