A web target, built by emcc, that a raylib example reaches unedited
This commit is contained in:
commit
1ea9456e2c
5
.gitignore
vendored
5
.gitignore
vendored
@ -45,3 +45,8 @@ old-ocaml/
|
||||
.claude/
|
||||
probe
|
||||
probe.c
|
||||
|
||||
# raylib built for the browser: a 1.6MB archive and the pinned checkout it
|
||||
# came from. vendor/raylib/build-web.sh makes both, and the path is named to a
|
||||
# build through FLAN_RAYLIB_WEB, not committed.
|
||||
vendor/raylib/web/
|
||||
|
||||
73
BUILT.md
73
BUILT.md
@ -242,7 +242,9 @@ Two claims that got run together in an earlier note, for the record:
|
||||
- *raylib does not work on wasm* — false. It works through emscripten. What is true is that it does not work on the
|
||||
**wasi** path, which is what the headless table targets, and which has no GL and no browser.
|
||||
- *a game loop cannot be expressed on wasm* — false. The browser cannot be blocked, so a web build drives the loop with
|
||||
`emscripten_set_main_loop` instead of a `while`. That is a different `main`, not a different program.
|
||||
`emscripten_set_main_loop` instead of a `while`. ~~That is a different `main`, not a different program.~~ **The premise
|
||||
held and the conclusion did not.** It is the same `main` and the same program: `-sASYNCIFY` answers the same browser
|
||||
fact without cutting anything in half. See "The browser is the third target" below.
|
||||
|
||||
**Three edits were made to sand.flan's own text** when it was ported, and they are language decisions rather than fixes:
|
||||
|
||||
@ -1272,5 +1274,70 @@ an `ExperimentalWarning` to stderr on every run and the harness compares combine
|
||||
one (same reason), and `flan run --target=` (a `.wasm` is not something this host execs — build it and point a runtime
|
||||
at it).
|
||||
|
||||
Still open: raylib on wasm, which plan.org wants through emscripten and its own sysroot. wasi-sdk is right for the
|
||||
headless table; it is not necessarily right for the eventual game build.
|
||||
~~Still open: raylib on wasm, which plan.org wants through emscripten and its own sysroot.~~ **Done — and it is a third
|
||||
target, not a mode of this one.** wasi-sdk is right for the headless table and was never going to be right for the game
|
||||
build. See the next section.
|
||||
|
||||
## The browser is the third target
|
||||
|
||||
`flan build --target=web` produces a page, its JS and a `.wasm`, and a raylib example opens in a browser from source
|
||||
that was not touched. The two wasm targets share the word and almost nothing else, so `is_wasi` and `is_web` are
|
||||
separate predicates and `is_wasm` is their union — the union is exactly the set of facts about the *machine* (32-bit
|
||||
pointers, no `dlopen`), which is what the refusals are about, and nothing else is shared.
|
||||
|
||||
**The compiler is `emcc`, not `clang`, and that is the whole of the sysroot story.** Everything the wasi target has to
|
||||
find by hand — a sysroot, a builtins archive, a shadow resource directory, the `__main_argc_argv` shim — is what emcc
|
||||
*is*. `target_flags` for `web` is the empty list; the only thing checked is that emcc exists, refused by name where the
|
||||
reason can say so. The one fact that had to be true for any of this: **emcc takes a `.ll` on its command line**, which
|
||||
it does, so `Emit`'s output needs no change and the IR stays target-independent. The object cache keys on the compiler
|
||||
binary's path, size and mtime as it always did — now of *whichever* compiler the target uses, so an emcc `flan_rt.o`
|
||||
and a clang one cannot collide.
|
||||
|
||||
**The main loop: `-sASYNCIFY`, not `emscripten_set_main_loop`.** The older note above had the browser fact right —
|
||||
it cannot be blocked — and drew the wrong conclusion from it. `emscripten_set_main_loop` wants the loop body as a
|
||||
callback, so every one of the eleven examples that writes
|
||||
|
||||
```lisp
|
||||
(until (rl/window-should-close?) ...)
|
||||
```
|
||||
|
||||
would have to be split by hand into an init and a tick, and the web program would stop being the native program.
|
||||
Asyncify rewrites the module so a call can suspend across a return to the event loop, and raylib's web platform is
|
||||
built for precisely that: `WindowShouldClose()` on `PLATFORM_WEB` is an `emscripten_sleep(16)` that then returns false
|
||||
(raylib 5.5, `platforms/rcore_web.c`, read rather than assumed). So the loop yields once a frame at a call it already
|
||||
makes, and **no example changed a character**. The price is real and is paid by every web build: asyncify instruments
|
||||
the whole module, roughly doubling code size. It is not applied per-program because "does this program block" is not a
|
||||
question `Build` can answer, and a flag set that varies per program is a cache key that varies per program.
|
||||
|
||||
**`link` lines can be addressed to a target.** `vendor/raylib/link` named `libraylib.so.550`, which exists on the host
|
||||
and nowhere else. A line may now carry `@native`, `@wasi` or `@web`, an untagged line applies everywhere — which is
|
||||
what every existing `link` file already is — and `${NAME}` expands from the environment. The selection happens in
|
||||
`Build` and not in `Load`, which is where the file is read, because **`Load` resolves imports before a target is
|
||||
chosen**: the same program is built for both, and a package's linker arguments arrive here as a flat list of strings.
|
||||
`Load`'s part in this is to pass the lines through untouched, which it already did.
|
||||
|
||||
**raylib for the browser is built, not installed.** No emscripten port provides it (`emcc --show-ports`: there is
|
||||
`contrib.glfw3` and no raylib), so `vendor/raylib/build-web.sh` clones raylib at the **5.5** tag — the one whose
|
||||
`.so.550` the host links, because `raylib.flan` carries raylib's struct layouts and enum values and two targets built
|
||||
from different raylibs would disagree about them in silence — and compiles the seven modules with
|
||||
`-DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES2` into one archive under `vendor/raylib/web/` (gitignored). `rglfw.c` is not
|
||||
among them: the web platform uses emscripten's own GLFW port, which is why `link` carries `@web -sUSE_GLFW=3`. No
|
||||
headers are installed, for the reason the host build needs none — the generated shim declares its own prototypes.
|
||||
|
||||
**The HTML shell is a string in `Build`, not a file in the tree**, for the same reason `Runtime_src` is: it has to be
|
||||
wherever the compiler is, and a build that cannot find its own shell fails for a reason nobody spelled. It is a canvas
|
||||
and a `Module.print` that puts stdout on the page; `FLAN_WEB_SHELL` replaces it. `--shell-file` is passed only when the
|
||||
output is a `.html`, because emcc accepts and ignores it otherwise.
|
||||
|
||||
**Refused by name, inherited whole:** `--dev`, `--debug`, `Build.shared` and `flan run --target=` are refused for
|
||||
`web` exactly as for `wasm32`, each naming `web` rather than `wasm32` in the message. `--sanitize` is refused too, but
|
||||
the web half of that refusal is weaker than the wasi half and says so: emscripten *does* ship an ASan, and nothing here
|
||||
has ever run it. A sanitizer that has never been run is one whose silence means nothing.
|
||||
|
||||
**What the test can honestly check.** `test/test_web.ml` is headless and permanently so. It probes — emscripten may not
|
||||
be installed, and the raylib archive is not in the tree — and skips with the reason rather than going red. What it
|
||||
asserts: the three files exist, the module starts with `\0asm`, the page references its own JS and carries the canvas,
|
||||
and node runs the emitted JS and gets `ok`. For raylib it builds `core-basic-window.flan` unchanged and then reads the
|
||||
module for the two things that would be false if the mechanism were wrong: an `asyncify_start_unwind` export, and a
|
||||
`glViewport` import that can only have come from raylib's web platform. Import and export names are plain strings in
|
||||
the binary, so this needs no wasm reader.
|
||||
|
||||
38
NEXT.md
38
NEXT.md
@ -295,6 +295,44 @@ crosses as a parameter — a C function that returns one returns something Flan
|
||||
the shape the language already has. Three constructs unexercised anywhere else in the repo worked first try: a fixed
|
||||
array with a struct element, a 2-D struct array, and `[N string]` as both `defconst` and mutable `defvar`.
|
||||
|
||||
### The web target: what it does not reach yet
|
||||
|
||||
`flan build --target=web` works, a raylib example builds unchanged and `test/test_web.ml` is green — see BUILT.md,
|
||||
"The browser is the third target", for the mechanism and why asyncify rather than `emscripten_set_main_loop`. Four
|
||||
things it does not cover.
|
||||
|
||||
**1. `sand.flan` has no web build, and the cause is one missing `#include`.** `vendor/agent/flan_agent.c` does not
|
||||
compile under emcc: *variable has incomplete type 'struct timeval'* at line 426, because emscripten's headers do not
|
||||
pull `<sys/time.h>` in transitively the way glibc's do. `sand.flan`'s `main` calls `(agent/start ...)`
|
||||
unconditionally, so `Reach` cannot prune the package, so the flagship program stops at that error — even without
|
||||
`--dev`. Beneath the include is a structural fact worth deciding rather than patching around: **the agent is a socket
|
||||
server and the browser has no sockets**, which is the same family as the `--dev` refusal. So the two fixes are not
|
||||
equivalent — add the include and the agent compiles into a web build that can never accept a connection, or refuse
|
||||
`vendor:agent` by name on a web target the way `--dev` is refused. The second is the honest one. Neither was taken
|
||||
here: `vendor/agent/` belonged to another lane this session.
|
||||
|
||||
**2. Assets are two questions and only one of them is about emscripten.** `sand.flan` does
|
||||
`(rl/load-texture "brush.png")` against a bare relative path.
|
||||
|
||||
- The easy half: a bare relative path has no meaning on a target with no filesystem. emscripten's answer is
|
||||
`--embed-file` or `--preload-file` into MEMFS, and both are *linker arguments*, so they are already expressible as an
|
||||
`@web` line in a package's `link` file. No new mechanism is needed for a package.
|
||||
- The hard half, and the actual design question: **the file that needs the asset is structurally the one file that
|
||||
cannot declare it.** `Load` hands out `lflags` only for a directory package (`one_file` → `[]`), and `main` is not
|
||||
exported, so a program can never be a package. The program doing the `load-texture` therefore has no link channel at
|
||||
all. Answering this means either giving a single-file program a way to carry build arguments, or making assets their
|
||||
own declaration rather than a linker flag. No flag was invented for it here.
|
||||
|
||||
**3. Nothing has been opened in a browser.** The test is headless and permanently so: it asserts the artifact's shape,
|
||||
the `asyncify_start_unwind` export and the `glViewport` import, and that node runs the emitted JS. Whether the canvas
|
||||
actually paints is unverified by anything in CI, and a human should look once.
|
||||
|
||||
**4. Unmeasured and untested.** Asyncify's cost is quoted from emscripten's documentation (roughly a doubling of code
|
||||
size) and not measured here, and no frame time on web has been taken at all. raylib's audio and any use of threads on
|
||||
the web target are untried. And a **wasi** build that reaches raylib now fails on undefined symbols rather than on a
|
||||
missing `-l:libraylib.so.550`, because that line is tagged `@native` — the same error one step later, and a worse
|
||||
message.
|
||||
|
||||
### `break`, and why it was not built
|
||||
|
||||
Settled, so the next attempt is cheap rather than a rediscovery:
|
||||
|
||||
11
bin/main.ml
11
bin/main.ml
@ -81,7 +81,8 @@ let sanitize_flag = "--sanitize"
|
||||
|
||||
let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag ]
|
||||
|
||||
(* [--target=wasm32-wasi], the one cross target. Unlike the flags above it
|
||||
(* [--target=wasm32-wasi] and [--target=web], the two cross targets. Unlike
|
||||
the flags above, a target
|
||||
carries a value, so it is matched by prefix and stripped from the residual
|
||||
arguments by the same test — otherwise [-o out --target=X] falls into the
|
||||
usage error. *)
|
||||
@ -187,13 +188,17 @@ let () =
|
||||
let base = Filename.remove_extension (Filename.basename path) in
|
||||
(* A wasm module is not an executable and must not be named like one:
|
||||
the extension is what tells a runtime, and a reader, what it is. *)
|
||||
(* A web build is three files — the page, its JS and the module — and
|
||||
the page is the one named here: emcc derives the other two from it,
|
||||
and it is the one a browser opens. *)
|
||||
(match target with
|
||||
| Some t when Flan.Build.is_web t -> base ^ ".html"
|
||||
| Some t when String.starts_with ~prefix:"wasm32" t -> base ^ ".wasm"
|
||||
| _ -> base)
|
||||
| _ ->
|
||||
prerr_endline
|
||||
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
||||
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi]";
|
||||
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web]";
|
||||
exit 2
|
||||
in
|
||||
with_errors path (fun () ->
|
||||
@ -285,7 +290,7 @@ let () =
|
||||
prerr_endline
|
||||
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
|
||||
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
||||
[--debug] [--sanitize] [--target=wasm32-wasi]\n\
|
||||
[--debug] [--sanitize] [--target=wasm32-wasi|web]\n\
|
||||
\ flan run <file.flan> [args...]\n\
|
||||
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
|
||||
\ flan dev <program.flan> [-s socket]";
|
||||
|
||||
321
lib/build.ml
321
lib/build.ml
@ -4,12 +4,24 @@
|
||||
|
||||
{v flan → typed IR → .ll → clang --target={native,wasm32} v}
|
||||
|
||||
Three targets now, and the third takes a different driver: the browser is
|
||||
built by emcc, which accepts the same .ll. See [is_web].
|
||||
|
||||
Not the dev path — that one never invokes the clang driver, because the
|
||||
driver *is* the cost, and goes llc + ld -shared + dlopen instead. That is
|
||||
[shared], at the bottom of this file, measured at ~19ms. *)
|
||||
|
||||
let clang = try Sys.getenv "FLAN_CLANG" with Not_found -> "clang"
|
||||
|
||||
(* The browser target's compiler. emcc is a clang driver with a sysroot, a
|
||||
builtins archive, a JS runtime and an HTML shell already attached, so the
|
||||
whole of [wasm_sysroot], [wasm_builtins] and [wasm_resource_dir] below —
|
||||
everything the wasi target has to find by hand — is what emcc *is*. It also
|
||||
accepts a .ll on its command line, which is the one thing that had to be
|
||||
true for this target to exist at all: [Emit] writes IR text and nothing
|
||||
else. *)
|
||||
let emcc = try Sys.getenv "FLAN_EMCC" with Not_found -> "emcc"
|
||||
|
||||
let write path contents =
|
||||
let ch = open_out path in
|
||||
output_string ch contents;
|
||||
@ -50,7 +62,10 @@ let cachedir () =
|
||||
d
|
||||
|
||||
type opts = {
|
||||
target : string option; (* None is the host; "wasm32-wasi" is the other *)
|
||||
(* None is the host. "wasm32-wasi" is the headless one, run by a WASI
|
||||
runtime; "web" is the browser one, built by emscripten. They share the
|
||||
word wasm and almost nothing else — see [is_wasi] and [is_web]. *)
|
||||
target : string option;
|
||||
opt : string;
|
||||
keep : bool; (* leave the .ll behind *)
|
||||
checks : bool; (* bounds-check [at] and [slice] *)
|
||||
@ -134,11 +149,38 @@ let cflags opts =
|
||||
|
||||
let getenv name = try Some (Sys.getenv name) with Not_found -> None
|
||||
|
||||
let is_wasm t = String.starts_with ~prefix:"wasm32" t
|
||||
(* ── Two wasm targets, spelled apart ─────────────────────────────────
|
||||
[web] is emscripten's: a browser, a GL context, a JS runtime, and a clang
|
||||
whose sysroot and builtins come with it. [wasm32-wasi] is the headless one:
|
||||
a WASI runtime, no GL, no browser, and a sysroot this file has to find.
|
||||
"wasm32-unknown-emscripten" is accepted as a synonym for [web] because that
|
||||
is the triple, and someone will write it.
|
||||
|
||||
What the two share is the machine — 32-bit pointers, no dlopen — which is
|
||||
exactly the set of things [is_wasm] guards: the three refusals below are
|
||||
about the machine and so they apply to both. *)
|
||||
let is_web t =
|
||||
t = "web" || t = "emscripten"
|
||||
|| String.starts_with ~prefix:"wasm32-unknown-emscripten" t
|
||||
|| String.starts_with ~prefix:"wasm32-emscripten" t
|
||||
|
||||
let is_wasi t = String.starts_with ~prefix:"wasm32" t && not (is_web t)
|
||||
let is_wasm t = is_web t || is_wasi t
|
||||
|
||||
let wasm_target opts =
|
||||
match opts.target with Some t when is_wasm t -> true | _ -> false
|
||||
|
||||
let web_target opts =
|
||||
match opts.target with Some t when is_web t -> true | _ -> false
|
||||
|
||||
let wasi_target opts =
|
||||
match opts.target with Some t when is_wasi t -> true | _ -> false
|
||||
|
||||
(* Which compiler a target is built by. This is not a flag difference: emcc is
|
||||
a different program with a different driver, and the object cache key below
|
||||
carries it for the same reason it carries clang's mtime. *)
|
||||
let compiler opts = if web_target opts then emcc else clang
|
||||
|
||||
(* Where on PATH a program is, or None. *)
|
||||
let on_path prog =
|
||||
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
||||
@ -256,6 +298,19 @@ let wasm_resource_dir () =
|
||||
let target_flags opts =
|
||||
match opts.target with
|
||||
| None -> []
|
||||
(* The browser target adds no flags to a compile at all: emcc already is the
|
||||
triple, the sysroot and the builtins. What it does need is to exist, and
|
||||
an emcc that is not there must be refused here — where the reason can name
|
||||
it — and not at the first "command not found" from a subshell. *)
|
||||
| Some t when is_web t ->
|
||||
if on_path emcc = None && not (Sys.file_exists emcc) then
|
||||
failwith
|
||||
(Printf.sprintf
|
||||
"web: no %s on PATH. The browser target is built by emscripten: \
|
||||
install an emsdk and source its emsdk_env.sh, or set FLAN_EMCC to \
|
||||
the emcc to use."
|
||||
emcc);
|
||||
[]
|
||||
| Some t when not (is_wasm t) -> [ "--target=" ^ t ]
|
||||
| Some t ->
|
||||
let sysroot = wasm_sysroot () in
|
||||
@ -285,23 +340,202 @@ let wasm_main_source =
|
||||
"int flan_entry(int argc, char **argv) __asm__(\"main\");\n\
|
||||
int __main_argc_argv(int argc, char **argv) { return flan_entry(argc, argv); }\n"
|
||||
|
||||
(* ── The browser: the main loop, the shell, and the link ─────────────
|
||||
The mechanism, and why it is not the one the old note predicted.
|
||||
|
||||
BUILT.md says a web build "drives the loop with [emscripten_set_main_loop]
|
||||
instead of a [while]. That is a different [main], not a different program."
|
||||
The first half is right about the browser and wrong about what it costs
|
||||
here: [emscripten_set_main_loop] wants the loop body as a callback, so every
|
||||
example that spells
|
||||
|
||||
(until (rl/window-should-close?) ...)
|
||||
|
||||
would have to be cut in half — an init and a tick — by hand, in every file,
|
||||
and the resulting program would no longer be the one that runs natively.
|
||||
|
||||
[-sASYNCIFY] is the other half of the same browser fact and costs none of
|
||||
that. It rewrites the module so a call can suspend and resume across a
|
||||
return to the event loop, and raylib's web platform is built for it:
|
||||
[WindowShouldClose] on PLATFORM_WEB is an [emscripten_sleep(16)] that then
|
||||
returns false (raylib 5.5, platforms/rcore_web.c). So the Flan [until] loop
|
||||
yields to the browser once a frame, at the call it already makes, and not
|
||||
one example changes. Same program, same [main], same source on both targets.
|
||||
|
||||
What it costs is real and worth writing down: asyncify instruments the whole
|
||||
module, which is roughly a doubling of code size and a measurable slowdown
|
||||
on the instrumented paths. It is applied to every web build rather than to
|
||||
the ones that block, because "does this program block" is not a question
|
||||
this file can answer and a flag set that varies per program is a cache key
|
||||
that varies per program. *)
|
||||
|
||||
let web_shell_default =
|
||||
{html|<!doctype html>
|
||||
<!-- The smallest page that makes a Flan web build openable: a canvas for
|
||||
raylib's GL context, and stdout on the page rather than only in the
|
||||
console, because a headless check and a human reader want the same text.
|
||||
emcc substitutes the module's JS for {{{ SCRIPT }}}; nothing else here is
|
||||
emscripten's. Replace it with FLAN_WEB_SHELL. -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>flan</title>
|
||||
<style>
|
||||
html, body { margin: 0; background: #14161a; color: #d8dee9;
|
||||
font: 13px/1.5 ui-monospace, monospace; }
|
||||
#canvas { display: block; margin: 0 auto; background: #000;
|
||||
outline: none; }
|
||||
#out { white-space: pre-wrap; padding: 8px 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" tabindex="-1"
|
||||
oncontextmenu="event.preventDefault()"></canvas>
|
||||
<pre id="out"></pre>
|
||||
<script>
|
||||
var out = document.getElementById('out');
|
||||
var Module = {
|
||||
canvas: document.getElementById('canvas'),
|
||||
print: function (t) { out.textContent += t + '\n'; },
|
||||
printErr: function (t) { out.textContent += t + '\n'; },
|
||||
};
|
||||
</script>
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
</html>
|
||||
|html}
|
||||
|
||||
(* The shell is a string here rather than a file in the tree for the same
|
||||
reason [Runtime_src] is: it has to be present wherever the compiler is, and
|
||||
a build that cannot find its own shell is a build that fails for a reason
|
||||
nobody spelled. FLAN_WEB_SHELL replaces it. *)
|
||||
let web_shell_file () =
|
||||
match getenv "FLAN_WEB_SHELL" with
|
||||
| Some p when Sys.file_exists p -> p
|
||||
| Some p ->
|
||||
failwith (Printf.sprintf "web: FLAN_WEB_SHELL is %s, which does not exist" p)
|
||||
| None ->
|
||||
let p = Filename.concat (workdir ()) "flan-shell.html" in
|
||||
write p web_shell_default;
|
||||
p
|
||||
|
||||
(* The flags the browser target adds at the link, and only at the link.
|
||||
|
||||
ALLOW_MEMORY_GROWTH because the default heap is 16MB and a texture is not
|
||||
small. EXPORT_ES6=0 and the default MODULARIZE are left alone so that the
|
||||
.js is a plain script both the shell and node can run — which is what makes
|
||||
the test headless.
|
||||
|
||||
--shell-file only when the output is a page. Asking emcc for a .js and
|
||||
handing it an HTML shell is accepted and ignored, which is the kind of
|
||||
silence this file tries not to produce. *)
|
||||
let web_link_flags ~out =
|
||||
[ "-sASYNCIFY"; "-sALLOW_MEMORY_GROWTH=1" ]
|
||||
@ (if Filename.extension out = ".html" then
|
||||
[ "--shell-file"; Filename.quote (web_shell_file ()) ]
|
||||
else [])
|
||||
|
||||
(* ── A [link] line may be addressed to one target ────────────────────
|
||||
`vendor/raylib/link` names Fedora's libraylib.so.550, which exists on the
|
||||
host and nowhere else; the browser wants a static archive built by
|
||||
emscripten and three -s flags besides. So a line may carry a tag:
|
||||
|
||||
@native -l:libraylib.so.550
|
||||
@web ${FLAN_RAYLIB_WEB}
|
||||
|
||||
and an untagged line applies to every target, which is what every existing
|
||||
`link` file is.
|
||||
|
||||
The selection happens *here* and not in [Load], which is where the file is
|
||||
read, because [Load] resolves imports before a target is chosen — the same
|
||||
program is built for both — and a package's linker arguments are carried to
|
||||
this function as a flat list of strings. [Load] passing the lines through
|
||||
untouched is the whole of its part in this.
|
||||
|
||||
${NAME} expands from the environment. An unset one is refused by name: the
|
||||
archive a web build needs is built once by vendor/raylib/build-web.sh and
|
||||
lives at a path only that machine knows, and the alternative to naming it
|
||||
here is a linker error about GLFW symbols. *)
|
||||
|
||||
let link_tags = [ "native"; "wasi"; "web" ]
|
||||
|
||||
let target_tag opts =
|
||||
if web_target opts then "web"
|
||||
else if wasi_target opts then "wasi"
|
||||
else "native"
|
||||
|
||||
(* ${NAME} → the environment's NAME. Nothing else is substituted: this is not a
|
||||
shell, and a linker argument containing a $ that is not a ${ is left alone
|
||||
rather than guessed at. *)
|
||||
let expand_vars ~where s =
|
||||
let b = Buffer.create (String.length s) in
|
||||
let n = String.length s in
|
||||
let rec go i =
|
||||
if i >= n then ()
|
||||
else if i + 1 < n && s.[i] = '$' && s.[i + 1] = '{' then
|
||||
match String.index_from_opt s (i + 2) '}' with
|
||||
| None -> Buffer.add_char b s.[i]; go (i + 1)
|
||||
| Some j ->
|
||||
let name = String.sub s (i + 2) (j - i - 2) in
|
||||
(match getenv name with
|
||||
| Some v -> Buffer.add_string b v
|
||||
| None ->
|
||||
failwith
|
||||
(Printf.sprintf
|
||||
"%s: the linker argument %s wants %s, which is not set in the \
|
||||
environment"
|
||||
where s name));
|
||||
go (j + 1)
|
||||
else (Buffer.add_char b s.[i]; go (i + 1))
|
||||
in
|
||||
go 0;
|
||||
Buffer.contents b
|
||||
|
||||
let select_lflags opts flags =
|
||||
let want = target_tag opts in
|
||||
List.filter_map
|
||||
(fun f ->
|
||||
if String.length f > 0 && f.[0] = '@' then begin
|
||||
let tag, rest =
|
||||
match String.index_opt f ' ' with
|
||||
| Some i ->
|
||||
(String.sub f 1 (i - 1),
|
||||
String.trim (String.sub f (i + 1) (String.length f - i - 1)))
|
||||
| None -> (String.sub f 1 (String.length f - 1), "")
|
||||
in
|
||||
if not (List.mem tag link_tags) then
|
||||
failwith
|
||||
(Printf.sprintf
|
||||
"link: @%s is not a target — the tags are %s, and an untagged \
|
||||
line applies to all of them"
|
||||
tag
|
||||
(String.concat ", " (List.map (fun t -> "@" ^ t) link_tags)));
|
||||
if tag = want && rest <> "" then
|
||||
Some (expand_vars ~where:("link: @" ^ tag) rest)
|
||||
else None
|
||||
end
|
||||
else Some (expand_vars ~where:"link" f))
|
||||
flags
|
||||
|
||||
(* What the compiler itself is, cheaply: its path, size and mtime. A clang
|
||||
upgrade changes one of those, so the key changes with it — without paying a
|
||||
[clang --version] subprocess on every build, which would cost most of what
|
||||
the cache buys. *)
|
||||
let clang_stamp =
|
||||
lazy
|
||||
(let path =
|
||||
if Filename.is_relative clang then
|
||||
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
||||
(try List.find (fun d -> Sys.file_exists (Filename.concat d clang))
|
||||
dirs |> fun d -> Filename.concat d clang
|
||||
with Not_found -> clang)
|
||||
else clang
|
||||
in
|
||||
match Unix.stat path with
|
||||
| st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime
|
||||
| exception Unix.Unix_error _ -> path)
|
||||
the cache buys. Taken of whichever compiler the target uses, so an emcc
|
||||
object and a clang object of the same source cannot collide. *)
|
||||
let stamp_of prog =
|
||||
let path =
|
||||
if Filename.is_relative prog then
|
||||
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
||||
(try List.find (fun d -> Sys.file_exists (Filename.concat d prog))
|
||||
dirs |> fun d -> Filename.concat d prog
|
||||
with Not_found -> prog)
|
||||
else prog
|
||||
in
|
||||
match Unix.stat path with
|
||||
| st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime
|
||||
| exception Unix.Unix_error _ -> path
|
||||
|
||||
let clang_stamp = lazy (stamp_of clang)
|
||||
|
||||
(* Compile one C translation unit to an object file, reusing a cached one when
|
||||
the source text, the compiler and the flags are all unchanged. The key has
|
||||
@ -312,11 +546,12 @@ let compile_c ~opts ?tflags ~src ~name () =
|
||||
resource directory decide which headers and which builtins an object was
|
||||
built against, so repointing either must not serve a stale .o. *)
|
||||
let tflags = match tflags with Some f -> f | None -> target_flags opts in
|
||||
let cc = compiler opts in
|
||||
let key =
|
||||
Digest.to_hex
|
||||
(Digest.string
|
||||
(String.concat "\000"
|
||||
[ name; src; Lazy.force clang_stamp; opts.opt;
|
||||
[ name; src; stamp_of cc; opts.opt;
|
||||
String.concat " " (cflags opts);
|
||||
String.concat " " tflags ]))
|
||||
in
|
||||
@ -330,14 +565,14 @@ let compile_c ~opts ?tflags ~src ~name () =
|
||||
let tmp = Printf.sprintf "%s.%d.tmp" obj (Unix.getpid ()) in
|
||||
let cmd =
|
||||
String.concat " "
|
||||
([ Filename.quote clang; opts.opt ]
|
||||
([ Filename.quote cc; opts.opt ]
|
||||
@ cflags opts
|
||||
@ [ "-c" ] @ tflags
|
||||
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
||||
in
|
||||
let code = Sys.command cmd in
|
||||
if code <> 0 then
|
||||
failwith (Printf.sprintf "%s failed (exit %d) on %s" clang code name);
|
||||
failwith (Printf.sprintf "%s failed (exit %d) on %s" cc code name);
|
||||
(try Unix.rename tmp obj with Unix.Unix_error _ -> ());
|
||||
(try Sys.remove c with Sys_error _ -> ())
|
||||
end;
|
||||
@ -351,10 +586,15 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
(* A dev build is the REPL's, and the REPL reaches a running process through
|
||||
[-rdynamic] and [dlopen]. Neither exists on wasm32, so the combination is
|
||||
refused rather than quietly producing a module nothing can attach to. *)
|
||||
(* Named for the target that was asked for, since two of them answer to
|
||||
[wasm_target] now and "wasm32:" on a --target=web build reads as a
|
||||
compiler that did not hear the question. *)
|
||||
let tname = if web_target opts then "web" else "wasm32" in
|
||||
if wasm_target opts && opts.dev then
|
||||
failwith
|
||||
"wasm32: --dev is native only — the reload path is dlopen, which wasm32 \
|
||||
has no equivalent of";
|
||||
(tname
|
||||
^ ": --dev is native only — the reload path is dlopen, which wasm has \
|
||||
no equivalent of");
|
||||
(* Refused rather than emitted-and-hoped-for. The member offsets in the DWARF
|
||||
are computed for the host's layout — [ptr] 8 bytes — and wasm32's pointer
|
||||
is 4, so a slice's [len] is at byte 8 there and at byte 16 here. Emitting
|
||||
@ -363,16 +603,26 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
keeps meeting at the FFI boundary. *)
|
||||
if wasm_target opts && opts.debug then
|
||||
failwith
|
||||
"wasm32: --debug is native only — the DWARF member offsets are computed \
|
||||
for the host's layout, and wasm32's 32-bit pointer moves every one of \
|
||||
them";
|
||||
(tname
|
||||
^ ": --debug is native only — the DWARF member offsets are computed \
|
||||
for the host's layout, and wasm32's 32-bit pointer moves every one \
|
||||
of them");
|
||||
(* There is no wasm32 sanitizer runtime to link against: clang accepts
|
||||
-fsanitize=address for the triple and the link fails on
|
||||
__asan_report_load4. Refused by name rather than met at the linker. *)
|
||||
(* emscripten does ship an ASan, so the web half of this refusal is weaker
|
||||
than the wasi half: it is untested here rather than known to be
|
||||
impossible. Refused all the same, because a sanitizer that has never been
|
||||
run is a sanitizer whose silence means nothing, and the sweep this project
|
||||
runs (@sanitize) is native. *)
|
||||
if wasm_target opts && opts.sanitize then
|
||||
failwith
|
||||
"wasm32: --sanitize is native only — there is no libclang_rt.asan for \
|
||||
wasm32-wasi to link against";
|
||||
(if web_target opts then
|
||||
"web: --sanitize is native only — emscripten has its own ASan, and \
|
||||
nothing here has ever run it; the sanitizer sweep is the native one"
|
||||
else
|
||||
"wasm32: --sanitize is native only — there is no libclang_rt.asan for \
|
||||
wasm32-wasi to link against");
|
||||
(* -O0 is not a choice a debug build offers: [llvm.dbg.declare] describes an
|
||||
alloca, and at -O2 mem2reg deletes the alloca. [sanitize] deliberately
|
||||
does not do this: see [opts]. *)
|
||||
@ -399,8 +649,11 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
let objs =
|
||||
cc Runtime_src.source "flan_rt.c"
|
||||
:: [ cc Runtime_src.dev_source "flan_dev.c" ]
|
||||
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source]. *)
|
||||
@ (if wasm_target opts then [ cc wasm_main_source "flan_wasm_main.c" ] else [])
|
||||
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source].
|
||||
Not the browser's: emscripten's start code calls [main] under that name,
|
||||
so the .ll's @main is already the entry point and the shim would be a
|
||||
second definition of it. *)
|
||||
@ (if wasi_target opts then [ cc wasm_main_source "flan_wasm_main.c" ] else [])
|
||||
(* The generated half of the FFI: one translation unit holding a typedef
|
||||
per struct that crosses and a wrapper per (declare-c ...), compiled
|
||||
exactly like a package's hand-written .c. It rides on the program
|
||||
@ -414,7 +667,7 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
in
|
||||
let cmd =
|
||||
String.concat " "
|
||||
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
||||
([ Filename.quote (compiler opts); opts.opt; "-Wno-override-module" ]
|
||||
(* -g at the link so clang does not strip, and keeps the object files'
|
||||
debug sections; the .ll carries its own. The sanitizer flags have to
|
||||
be here too — they are what pulls in libclang_rt.asan and the UBSan
|
||||
@ -424,9 +677,12 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
@ cflags opts
|
||||
@ (if opts.dev then [ "-rdynamic" ] else [])
|
||||
@ tflags
|
||||
@ (if web_target opts then web_link_flags ~out else [])
|
||||
@ [ Filename.quote ll ]
|
||||
@ List.map Filename.quote objs
|
||||
@ lflags
|
||||
(* A package's linker arguments, with the lines addressed to another
|
||||
target dropped and ${VAR} expanded. See [select_lflags]. *)
|
||||
@ select_lflags opts lflags
|
||||
(* The prelude declares sqrtf, so every link needs libm. It goes here
|
||||
and not in the leading flags: the default --as-needed drops a
|
||||
library named before the object that wants it, so at -O2 this would
|
||||
@ -480,8 +736,9 @@ let shared ?(opts = default) ~ir ~out () : timing =
|
||||
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
|
||||
if wasm_target opts then
|
||||
failwith
|
||||
"wasm32: the reload path is native only — it is llc + ld -shared + \
|
||||
dlopen, and wasm32 has no dlopen";
|
||||
((if web_target opts then "web" else "wasm32")
|
||||
^ ": the reload path is native only — it is llc + ld -shared + dlopen, \
|
||||
and wasm has no dlopen");
|
||||
let dir = workdir () in
|
||||
let base = Filename.remove_extension (Filename.basename out) in
|
||||
let ll = Filename.concat dir (base ^ ".ll") in
|
||||
|
||||
17
test/dune
17
test/dune
@ -33,6 +33,23 @@
|
||||
; wasmer is installed.
|
||||
(file wasm-run.mjs)))
|
||||
|
||||
; The web target, in its own stanza rather than in the table above because it
|
||||
; is the one case whose toolchain is a separate install: emscripten, and a
|
||||
; raylib archive built by vendor/raylib/build-web.sh. It probes for both and
|
||||
; skips with the reason, so it is green on a machine that has neither.
|
||||
(test
|
||||
(name test_web)
|
||||
(modules test_web)
|
||||
(libraries flan unix)
|
||||
(deps
|
||||
(glob_files programs/*.flan)
|
||||
; The raylib bindings and the ported example the raylib case builds. The
|
||||
; example imports examples/digits.flan, so the directory comes whole.
|
||||
(glob_files %{workspace_root}/vendor/raylib/*)
|
||||
(glob_files %{workspace_root}/examples/*)
|
||||
; flan run --target=web is refused by the CLI, so the CLI has to be here.
|
||||
(file %{workspace_root}/bin/main.exe)))
|
||||
|
||||
; The corpus a second time under ASan and UBSan. Its own alias and not part of
|
||||
; `dune test`: a sanitized build is a statically linked 1.8MB binary that takes
|
||||
; tens of seconds to produce, so the sweep is minutes against the existing
|
||||
|
||||
229
test/test_web.ml
Normal file
229
test/test_web.ml
Normal file
@ -0,0 +1,229 @@
|
||||
(* The web target: a Flan program built for the browser by emscripten.
|
||||
|
||||
Headless, on purpose and permanently. Nothing here drives a browser; what a
|
||||
test can honestly say about a page it never opens is that the artifact is
|
||||
the shape a browser needs — three files, a wasm that is a wasm, a page that
|
||||
loads the JS beside it — and that the module has the two things the target
|
||||
is *for*: asyncify, so a Flan `until` loop can yield to the event loop, and
|
||||
raylib's GL imports, so the loop has something to draw with.
|
||||
|
||||
Everything is probed rather than assumed, the same way the wasm32 case in
|
||||
test_acceptance is: emscripten may not be installed, and the raylib archive
|
||||
is built once by vendor/raylib/build-web.sh and is not in the tree. A missing
|
||||
piece is a skip with the reason, never a red test. *)
|
||||
|
||||
open Flan
|
||||
|
||||
let failures = ref 0
|
||||
|
||||
let scratch = Filename.get_temp_dir_name ()
|
||||
|
||||
let fail fmt = Printf.ksprintf (fun s -> incr failures; print_endline ("FAIL " ^ s)) fmt
|
||||
|
||||
let read path =
|
||||
let ch = open_in_bin path in
|
||||
let n = in_channel_length ch in
|
||||
let s = really_input_string ch n in
|
||||
close_in ch; s
|
||||
|
||||
let contains hay needle =
|
||||
let n = String.length needle and h = String.length hay in
|
||||
let rec go i = i + n <= h && (String.sub hay i n = needle || go (i + 1)) in
|
||||
go 0
|
||||
|
||||
let have prog = Sys.command (Printf.sprintf "command -v %s > /dev/null 2>&1" prog) = 0
|
||||
|
||||
(* One web build, through [Load] and [Reach] exactly as [flan build] does it,
|
||||
so a package's per-target link lines are selected here too. *)
|
||||
let web_build ?(opt = "-O2") path out =
|
||||
let l = Load.program ~file:path (Parse.program (Reader.read_file path)) in
|
||||
let p = Check.program l.Load.decls in
|
||||
let p, csrcs, lflags = Reach.link l p in
|
||||
ignore
|
||||
(Build.executable ~opts:{ Build.default with opt; target = Some "web" }
|
||||
~csrcs ~lflags p ~out)
|
||||
|
||||
(* emcc derives the JS and the module from the page's name. *)
|
||||
let parts html =
|
||||
let base = Filename.remove_extension html in
|
||||
(html, base ^ ".js", base ^ ".wasm")
|
||||
|
||||
let cleanup html =
|
||||
let a, b, c = parts html in
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ a; b; c ]
|
||||
|
||||
(* The archive vendor/raylib/build-web.sh makes. FLAN_RAYLIB_WEB is how a
|
||||
`link` line names it; the two relative paths are where the script puts it by
|
||||
default, seen from a test run out of _build. Found here and put back into
|
||||
the environment, so that a developer who has built it once does not also
|
||||
have to remember to export it before running the suite. That putenv outlives
|
||||
this call, so the raylib case being the last web build in the process is
|
||||
load-bearing: anything built after it would pick the archive up silently. *)
|
||||
let raylib_web () =
|
||||
let cands =
|
||||
(match Sys.getenv_opt "FLAN_RAYLIB_WEB" with Some s -> [ s ] | None -> [])
|
||||
@ [ "../../../vendor/raylib/web/libraylib-5.5.a";
|
||||
"../vendor/raylib/web/libraylib-5.5.a" ]
|
||||
in
|
||||
match List.find_opt Sys.file_exists cands with
|
||||
| Some p ->
|
||||
let p = try Unix.realpath p with Unix.Unix_error _ -> p in
|
||||
Unix.putenv "FLAN_RAYLIB_WEB" p;
|
||||
Some p
|
||||
| None -> None
|
||||
|
||||
let () =
|
||||
(* ── The probe ─────────────────────────────────────────────────────
|
||||
The smallest program there is, built for the browser. If emscripten is
|
||||
absent this is where it says so, and nothing below runs. *)
|
||||
let probe = Filename.concat scratch "flan-web-probe.html" in
|
||||
let outcome =
|
||||
match web_build "programs/unit-main.flan" probe with
|
||||
| () -> Ok ()
|
||||
| exception Failure m -> Error m
|
||||
in
|
||||
(match outcome with
|
||||
| Error why -> Printf.printf "web: skipping the web target (%s)\n" why
|
||||
| Ok () ->
|
||||
let html, js, wasm = parts probe in
|
||||
|
||||
(* Three files, because that is what "openable" means: a page, the JS that
|
||||
instantiates the module, and the module. *)
|
||||
List.iter
|
||||
(fun (what, f) ->
|
||||
if not (Sys.file_exists f) then fail "web build made no %s (%s)" what f)
|
||||
[ ("page", html); ("JS", js); ("module", wasm) ];
|
||||
|
||||
(* A wasm is a wasm. The four bytes are the whole format's claim about
|
||||
itself, and an emcc that produced JS and a stub would pass every other
|
||||
check here. *)
|
||||
let bytes = read wasm in
|
||||
if String.length bytes < 8 || String.sub bytes 0 4 <> "\000asm" then
|
||||
fail "the module does not start with the wasm magic";
|
||||
|
||||
(* The page has to load the module's JS, or it is a page about nothing.
|
||||
emcc minifies the shell, so this asks for the reference and not for any
|
||||
particular spelling of the tag. *)
|
||||
let page = read html in
|
||||
let base = Filename.basename (Filename.remove_extension probe) in
|
||||
if not (contains page (base ^ ".js")) then
|
||||
fail "the page does not reference %s.js" base;
|
||||
(* And it has to be *our* shell: the canvas raylib draws into, and the
|
||||
Module.print that puts stdout on the page. *)
|
||||
if not (contains page "canvas") then fail "the page has no canvas";
|
||||
|
||||
(* The one execution this test does. node runs the emitted JS without a
|
||||
DOM, which is enough for a program that only prints — and it is the
|
||||
same "does it actually run" the wasm32 case insists on. *)
|
||||
if not (have "node") then
|
||||
print_endline "web: skipping the run (no node)"
|
||||
else begin
|
||||
let out = Filename.concat scratch "flan-web-probe.out" in
|
||||
let code =
|
||||
Sys.command
|
||||
(Printf.sprintf "node %s > %s 2>&1" (Filename.quote js)
|
||||
(Filename.quote out))
|
||||
in
|
||||
let text = In_channel.with_open_bin out In_channel.input_all in
|
||||
(try Sys.remove out with Sys_error _ -> ());
|
||||
if code <> 0 || text <> "ok\n" then
|
||||
fail "node could not run the web build: %S (exit %d)" text code
|
||||
end;
|
||||
cleanup probe;
|
||||
|
||||
(* ── raylib in the browser ────────────────────────────────────────
|
||||
The claim BUILT.md left open. core-basic-window.flan is built for the
|
||||
web unchanged — no edit to its `until` loop, which is the whole point
|
||||
of choosing asyncify over emscripten_set_main_loop — and the module is
|
||||
then read for the two things that prove the claim rather than assert
|
||||
it: the asyncify export, and a GL import that can only have come from
|
||||
raylib's web platform.
|
||||
|
||||
Import and export names are plain strings in a wasm's name sections,
|
||||
which is why this needs no wasm reader. *)
|
||||
(match raylib_web () with
|
||||
| None ->
|
||||
print_endline
|
||||
"web: skipping the raylib case (no libraylib-5.5.a for the browser; \
|
||||
run sh vendor/raylib/build-web.sh)"
|
||||
| Some _ ->
|
||||
let out = Filename.concat scratch "flan-web-window.html" in
|
||||
(match web_build "../examples/core-basic-window.flan" out with
|
||||
| exception Failure m -> fail "raylib for the browser: %s" m
|
||||
| () ->
|
||||
let _, _, wasm = parts out in
|
||||
let bytes = read wasm in
|
||||
if not (contains bytes "asyncify_start_unwind") then
|
||||
fail
|
||||
"no asyncify in the module — the `until` loop would block the \
|
||||
browser";
|
||||
if not (contains bytes "glViewport") then
|
||||
fail "no GL imports in the module — raylib did not link";
|
||||
cleanup out)));
|
||||
|
||||
(* ── Refused by name ───────────────────────────────────────────────
|
||||
The web target inherits every refusal wasm32 has, and for the same
|
||||
reasons: the reload path is dlopen, the DWARF offsets are the host's, and
|
||||
the sanitizer sweep is native. These are asserted because "it falls out of
|
||||
the existing predicate" is exactly the kind of thing that stops being true
|
||||
silently. No emscripten is needed — each is refused before any compiler
|
||||
runs.
|
||||
|
||||
Each case names the phrase it expects and not merely the word "web". On a
|
||||
machine with no emscripten the *first* thing every one of these paths
|
||||
meets is "web: no emcc on PATH", which contains the word and is a
|
||||
different failure entirely — so a looser check would report all four as
|
||||
refused on exactly the machine where none of them ran. *)
|
||||
let refused what why f =
|
||||
match f () with
|
||||
| () -> fail "%s was accepted" what
|
||||
| exception Failure m ->
|
||||
if not (contains m "web") then
|
||||
fail "%s was refused, but the reason does not name the target: %S" what m
|
||||
else if not (contains m why) then
|
||||
fail "%s was refused for the wrong reason: wanted %S, said %S" what why m
|
||||
in
|
||||
let unit_main () =
|
||||
let path = "programs/unit-main.flan" in
|
||||
let l = Load.program ~file:path (Parse.program (Reader.read_file path)) in
|
||||
Check.program l.Load.decls
|
||||
in
|
||||
refused "--dev --target=web" "--dev is native only" (fun () ->
|
||||
ignore
|
||||
(Build.executable ~opts:{ Build.default with target = Some "web"; dev = true }
|
||||
(unit_main ()) ~out:(Filename.concat scratch "flan-web-dev.html")));
|
||||
refused "--debug --target=web" "--debug is native only" (fun () ->
|
||||
ignore
|
||||
(Build.executable
|
||||
~opts:{ Build.default with target = Some "web"; debug = true }
|
||||
(unit_main ()) ~out:(Filename.concat scratch "flan-web-dbg.html")));
|
||||
refused "--sanitize --target=web" "--sanitize is native only" (fun () ->
|
||||
ignore
|
||||
(Build.executable
|
||||
~opts:{ Build.default with target = Some "web"; sanitize = true }
|
||||
(unit_main ()) ~out:(Filename.concat scratch "flan-web-san.html")));
|
||||
refused "Build.shared --target=web" "reload path is native only" (fun () ->
|
||||
ignore
|
||||
(Build.shared ~opts:{ Build.default with target = Some "web" } ~ir:""
|
||||
~out:(Filename.concat scratch "flan-web.so") ()));
|
||||
|
||||
(* [flan run --target=web] is refused by the CLI rather than by [Build]: a
|
||||
page is not something this host execs, and picking a browser for it is not
|
||||
a decision that command has any business making. *)
|
||||
let out = Filename.concat scratch "flan-web-run.out" in
|
||||
let code =
|
||||
Sys.command
|
||||
(Printf.sprintf
|
||||
"../bin/main.exe run programs/unit-main.flan --target=web > %s 2>&1"
|
||||
(Filename.quote out))
|
||||
in
|
||||
let text = In_channel.with_open_bin out In_channel.input_all in
|
||||
(try Sys.remove out with Sys_error _ -> ());
|
||||
if code = 0 || not (contains text "--target is refused") then
|
||||
fail "flan run --target=web was not refused: %S (exit %d)" text code;
|
||||
|
||||
if !failures = 0 then print_endline "web: ok"
|
||||
else begin
|
||||
Printf.printf "web: %d failure(s)\n" !failures;
|
||||
exit 1
|
||||
end
|
||||
76
vendor/raylib/build-web.sh
vendored
Normal file
76
vendor/raylib/build-web.sh
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
#!/bin/sh
|
||||
# raylib, built for the browser.
|
||||
#
|
||||
# The host half of this package needs no build: Fedora ships libraylib.so.550
|
||||
# and `link` names it. The browser has no such thing, so the archive has to be
|
||||
# made here, once, out of raylib's own sources with emscripten's clang.
|
||||
#
|
||||
# Pinned to the tag whose shared library the host links — 5.5 against
|
||||
# libraylib.so.550 — because `raylib.flan` carries struct layouts and enum
|
||||
# values that are raylib's, not ours, and a build where the two targets are
|
||||
# different raylibs would disagree about them silently.
|
||||
#
|
||||
# The output is a plain static archive plus nothing else: no headers are
|
||||
# installed, because the generated FFI shim declares the prototypes it uses
|
||||
# (see BUILT.md, "No raylib headers are needed").
|
||||
#
|
||||
# sh vendor/raylib/build-web.sh
|
||||
#
|
||||
# It prints the line to export. `vendor/raylib/link` points at the archive
|
||||
# through ${FLAN_RAYLIB_WEB}, and a web build that names raylib with the
|
||||
# variable unset is refused by name rather than met at the linker.
|
||||
#
|
||||
# Environment:
|
||||
# FLAN_RAYLIB_TAG the raylib tag to build (default 5.5)
|
||||
# FLAN_RAYLIB_WEB_DIR where to put it (default vendor/raylib/web)
|
||||
# FLAN_RAYLIB_SRC an existing raylib checkout to build instead of cloning
|
||||
set -eu
|
||||
|
||||
tag=${FLAN_RAYLIB_TAG:-5.5}
|
||||
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
out=${FLAN_RAYLIB_WEB_DIR:-$here/web}
|
||||
archive=$out/libraylib-$tag.a
|
||||
|
||||
command -v emcc > /dev/null 2>&1 || {
|
||||
echo "build-web.sh: no emcc on PATH. Source an emsdk's emsdk_env.sh." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -f "$archive" ]; then
|
||||
echo "already built: $archive" >&2
|
||||
else
|
||||
mkdir -p "$out"
|
||||
if [ -n "${FLAN_RAYLIB_SRC:-}" ]; then
|
||||
src=$FLAN_RAYLIB_SRC
|
||||
else
|
||||
src=$out/raylib-$tag
|
||||
[ -d "$src" ] || git clone --depth 1 --branch "$tag" \
|
||||
https://github.com/raysan5/raylib "$src"
|
||||
fi
|
||||
|
||||
objs=$out/obj-$tag
|
||||
rm -rf "$objs"
|
||||
mkdir -p "$objs"
|
||||
|
||||
# GRAPHICS_API_OPENGL_ES2 is what WebGL is. PLATFORM_WEB makes rcore.c
|
||||
# include platforms/rcore_web.c, whose WindowShouldClose() is an
|
||||
# emscripten_sleep that returns false — see BUILT.md on why that is the whole
|
||||
# reason a Flan `until` loop needs no rewriting for the browser.
|
||||
#
|
||||
# rglfw.c is not in the list: the web platform uses emscripten's own GLFW
|
||||
# (-sUSE_GLFW=3, in `link`), not a compiled-in one.
|
||||
for m in rcore rshapes rtextures rtext rmodels raudio utils; do
|
||||
[ -f "$src/src/$m.c" ] || continue
|
||||
echo " emcc $m.c" >&2
|
||||
(cd "$src/src" && emcc -c -O2 -std=gnu99 \
|
||||
-DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES2 \
|
||||
-I. -Iexternal/glfw/include \
|
||||
"$m.c" -o "$objs/$m.o")
|
||||
done
|
||||
|
||||
emar rcs "$archive" "$objs"/*.o
|
||||
rm -rf "$objs"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "export FLAN_RAYLIB_WEB=$archive"
|
||||
33
vendor/raylib/link
vendored
33
vendor/raylib/link
vendored
@ -1,2 +1,31 @@
|
||||
-l:libraylib.so.550
|
||||
-lm
|
||||
# Extra linker arguments for this package, one per line. A line may be
|
||||
# addressed to one target — @native, @wasi, @web — and an untagged line
|
||||
# applies to all of them. ${NAME} expands from the environment. The selection
|
||||
# and the expansion happen in Build, which is the only place that knows which
|
||||
# target is being built; Load reads these lines and passes them through.
|
||||
|
||||
# The host. Fedora's package installs the versioned soname and no unversioned
|
||||
# symlink, so -lraylib finds nothing and the file has to be named.
|
||||
@native -l:libraylib.so.550
|
||||
@native -lm
|
||||
|
||||
# The browser. No shared library exists for wasm, so this is a static archive
|
||||
# built out of raylib's own sources by vendor/raylib/build-web.sh, pinned to
|
||||
# the 5.5 tag that matches the host's .so.550 — raylib.flan carries raylib's
|
||||
# struct layouts and enum values, and two targets built from different raylibs
|
||||
# would disagree about them without saying so.
|
||||
#
|
||||
# FLAN_RAYLIB_WEB is where that archive is. Unset, a web build that reaches
|
||||
# raylib is refused by name here rather than met as a page of undefined GLFW
|
||||
# symbols; build-web.sh prints the line to export.
|
||||
@web ${FLAN_RAYLIB_WEB}
|
||||
|
||||
# raylib's web platform is GLFW on emscripten's own port, not the rglfw.c it
|
||||
# compiles in natively, and WebGL is GLES2. GL_ENABLE_GET_PROC_ADDRESS because
|
||||
# rlgl asks for extension pointers by name.
|
||||
@web -sUSE_GLFW=3
|
||||
@web -sGL_ENABLE_GET_PROC_ADDRESS
|
||||
|
||||
# -sASYNCIFY is not here: Build adds it to every web link, because the reason
|
||||
# for it is the browser's event loop and not raylib. See Build's comment on the
|
||||
# main loop.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user