A target is more than a triple, so build.ml learns the rest
wasm32-wasi needs a sysroot clang does not know about and a builtins archive Fedora does not ship, and both have to reach the C compiles as well as the link — flan_rt.c includes <stdio.h> and never got past it. The flags are computed once and the whole list, not just the triple, is in the object cache key: repointing a sysroot must not be served a stale .o. Fedora ships no wasm libclang_rt.builtins.a and clang's resource directory is root-owned, so a shadow one is built under the object cache with the archive under the name clang looks for. The archive substituted is emscripten's libcompiler_rt.a, a different triple built by a different clang; wasi-sdk is the proper article and the comment says so, because a session reading "wasm32 works" should know which joint is glued. Nothing found means a refusal naming every path tried. The entry point is the other thing no triple tells you: wasi-libc calls __main_argc_argv, the .ll says @main, and the mismatch links clean and then traps on a weak stub. Two lines of C bridge it, and the asm label in them is why the shim is not an infinite self-call. --dev and Build.shared are refused for the target rather than half-supported: both are dlopen, which wasm32 has no equivalent of.
This commit is contained in:
parent
386d9e0372
commit
8e074bf0e2
212
lib/build.ml
212
lib/build.ml
@ -15,6 +15,21 @@ let write path contents =
|
|||||||
output_string ch contents;
|
output_string ch contents;
|
||||||
close_out ch
|
close_out ch
|
||||||
|
|
||||||
|
let write_bin path contents =
|
||||||
|
let ch = open_out_bin path in
|
||||||
|
output_string ch contents;
|
||||||
|
close_out ch
|
||||||
|
|
||||||
|
let read_file 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 read_file_opt path =
|
||||||
|
try Some (read_file path) with Sys_error _ -> None
|
||||||
|
|
||||||
(* One temporary directory per build, so the .ll is findable by name when
|
(* One temporary directory per build, so the .ll is findable by name when
|
||||||
something is wrong with it. *)
|
something is wrong with it. *)
|
||||||
let workdir () =
|
let workdir () =
|
||||||
@ -53,6 +68,159 @@ type opts = {
|
|||||||
let default =
|
let default =
|
||||||
{ target = None; opt = "-O2"; keep = false; checks = true; dev = false }
|
{ target = None; opt = "-O2"; keep = false; checks = true; dev = false }
|
||||||
|
|
||||||
|
(* ── wasm32, which needs more than a triple ──────────────────────────
|
||||||
|
The native target is whatever clang was built for, so [--target=] alone is
|
||||||
|
the whole of it. wasm32-wasi is not: the headers come from a sysroot clang
|
||||||
|
does not know about, and the builtins archive is not in clang's resource
|
||||||
|
directory on Fedora at all. Both have to be found, and *both* have to reach
|
||||||
|
the C compiles as well as the link — [flan_rt.c] includes <stdio.h>.
|
||||||
|
|
||||||
|
Anything missing is refused by name, with the path that is missing and the
|
||||||
|
package that would supply it. A build that reports success for a target it
|
||||||
|
cannot actually produce is the one outcome worth avoiding here. *)
|
||||||
|
|
||||||
|
let getenv name = try Some (Sys.getenv name) with Not_found -> None
|
||||||
|
|
||||||
|
let is_wasm t = String.starts_with ~prefix:"wasm32" t
|
||||||
|
|
||||||
|
let wasm_target opts =
|
||||||
|
match opts.target with Some t when is_wasm t -> true | _ -> false
|
||||||
|
|
||||||
|
(* 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
|
||||||
|
List.find_map
|
||||||
|
(fun d ->
|
||||||
|
let p = Filename.concat d prog in
|
||||||
|
if Sys.file_exists p then Some p else None)
|
||||||
|
dirs
|
||||||
|
|
||||||
|
let wasm_sysroot () =
|
||||||
|
match getenv "FLAN_WASM_SYSROOT" with Some s -> s | None -> "/usr/wasm32-wasi"
|
||||||
|
|
||||||
|
(* The builtins archive — __muldi3, the float conversions, memcpy. Fedora's
|
||||||
|
clang ships no wasm copy of it (dnf provides '*libclang_rt.builtins*wasm*'
|
||||||
|
finds nothing) and the proper article comes from a wasi-sdk release.
|
||||||
|
Failing that, emscripten builds the same compiler-rt for wasm32 and calls it
|
||||||
|
libcompiler_rt.a; it is a different triple (wasm32-unknown-emscripten) built
|
||||||
|
by a different clang, and it is *substituting* here, not the real thing. It
|
||||||
|
links and runs, and the sand hash matches native byte for byte, but a
|
||||||
|
session reading this should know the joint is glued. *)
|
||||||
|
let wasm_builtins_candidates () =
|
||||||
|
[ "/opt/wasi-sdk/lib/clang/20/lib/wasm32-unknown-wasi/libclang_rt.builtins.a" ]
|
||||||
|
@ (match on_path "emcc" with
|
||||||
|
| None -> []
|
||||||
|
| Some e ->
|
||||||
|
[ Filename.concat (Filename.dirname e)
|
||||||
|
"cache/sysroot/lib/wasm32-emscripten/libcompiler_rt.a" ])
|
||||||
|
|
||||||
|
let wasm_builtins () =
|
||||||
|
(* An explicit FLAN_WASM_BUILTINS that does not exist is an error and not a
|
||||||
|
hint: falling back to a guess would build against something other than
|
||||||
|
what was asked for and say nothing. *)
|
||||||
|
match getenv "FLAN_WASM_BUILTINS" with
|
||||||
|
| Some s when Sys.file_exists s -> s
|
||||||
|
| Some s ->
|
||||||
|
failwith (Printf.sprintf "wasm32: FLAN_WASM_BUILTINS is %s, which does not exist" s)
|
||||||
|
| None ->
|
||||||
|
let cands = wasm_builtins_candidates () in
|
||||||
|
match List.find_opt Sys.file_exists cands with
|
||||||
|
| Some p -> p
|
||||||
|
| None ->
|
||||||
|
failwith
|
||||||
|
(Printf.sprintf
|
||||||
|
"wasm32: no builtins archive. clang wants \
|
||||||
|
<resource-dir>/lib/wasm32-unknown-wasi/libclang_rt.builtins.a, which \
|
||||||
|
no Fedora package provides; it comes from a wasi-sdk release, or \
|
||||||
|
emscripten's libcompiler_rt.a will substitute. Looked in: %s. Set \
|
||||||
|
FLAN_WASM_BUILTINS to the archive."
|
||||||
|
(String.concat ", " cands))
|
||||||
|
|
||||||
|
(* clang's own resource directory, asked for rather than guessed — the version
|
||||||
|
number is in the path and a Fedora clang bump changes it. *)
|
||||||
|
let clang_resource_dir =
|
||||||
|
lazy
|
||||||
|
(let tmp =
|
||||||
|
Filename.concat (Filename.get_temp_dir_name ())
|
||||||
|
(Printf.sprintf "flan-rd-%d" (Unix.getpid ()))
|
||||||
|
in
|
||||||
|
let code =
|
||||||
|
Sys.command
|
||||||
|
(Printf.sprintf "%s -print-resource-dir > %s 2>/dev/null"
|
||||||
|
(Filename.quote clang) (Filename.quote tmp))
|
||||||
|
in
|
||||||
|
let s = if code = 0 then read_file_opt tmp else None in
|
||||||
|
(try Sys.remove tmp with Sys_error _ -> ());
|
||||||
|
match s with
|
||||||
|
| Some s -> String.trim s
|
||||||
|
| None -> failwith "wasm32: clang -print-resource-dir failed")
|
||||||
|
|
||||||
|
(* A resource directory clang will accept for wasm32-wasi: its real include
|
||||||
|
directory, and the builtins archive under the name and triple clang looks
|
||||||
|
for. Built under the object cache and named by a digest of what went into
|
||||||
|
it, so repointing FLAN_WASM_BUILTINS or upgrading clang makes a new one
|
||||||
|
rather than reusing a stale one. *)
|
||||||
|
let wasm_resource_dir () =
|
||||||
|
let real = Lazy.force clang_resource_dir in
|
||||||
|
let builtins = wasm_builtins () in
|
||||||
|
let st = Unix.stat builtins in
|
||||||
|
let key =
|
||||||
|
Digest.to_hex
|
||||||
|
(Digest.string
|
||||||
|
(String.concat "\000"
|
||||||
|
[ real; builtins; string_of_int st.Unix.st_size;
|
||||||
|
string_of_float st.Unix.st_mtime ]))
|
||||||
|
in
|
||||||
|
let dir = Filename.concat (cachedir ()) ("wasm-rd-" ^ key) in
|
||||||
|
let lib = Filename.concat dir "lib" in
|
||||||
|
let triple = Filename.concat lib "wasm32-unknown-wasi" in
|
||||||
|
let archive = Filename.concat triple "libclang_rt.builtins.a" in
|
||||||
|
if not (Sys.file_exists archive) then begin
|
||||||
|
let mk d = try Unix.mkdir d 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> () in
|
||||||
|
mk dir; mk lib; mk triple;
|
||||||
|
let inc = Filename.concat dir "include" in
|
||||||
|
if not (Sys.file_exists inc) then
|
||||||
|
(try Unix.symlink (Filename.concat real "include") inc
|
||||||
|
with Unix.Unix_error _ -> ());
|
||||||
|
let tmp = Printf.sprintf "%s.%d.tmp" archive (Unix.getpid ()) in
|
||||||
|
write_bin tmp (read_file builtins);
|
||||||
|
(try Unix.rename tmp archive with Unix.Unix_error _ -> ())
|
||||||
|
end;
|
||||||
|
dir
|
||||||
|
|
||||||
|
(* The flags a target adds, used by the C compiles and by the link alike. *)
|
||||||
|
let target_flags opts =
|
||||||
|
match opts.target with
|
||||||
|
| None -> []
|
||||||
|
| Some t when not (is_wasm t) -> [ "--target=" ^ t ]
|
||||||
|
| Some t ->
|
||||||
|
let sysroot = wasm_sysroot () in
|
||||||
|
(* Fedora's wasi-libc puts the headers one level deeper than wasi-sdk's
|
||||||
|
does — include/wasm32-wasi/stdio.h against include/stdio.h — so both
|
||||||
|
shapes count as a sysroot. clang finds either on its own. *)
|
||||||
|
if not (List.exists
|
||||||
|
(fun p -> Sys.file_exists (Filename.concat sysroot p))
|
||||||
|
[ "include/stdio.h"; "include/wasm32-wasi/stdio.h" ])
|
||||||
|
then
|
||||||
|
failwith
|
||||||
|
(Printf.sprintf
|
||||||
|
"wasm32: no sysroot at %s (wanted include/stdio.h). Install \
|
||||||
|
wasi-libc-devel and wasi-libc-static, or set FLAN_WASM_SYSROOT."
|
||||||
|
sysroot);
|
||||||
|
[ "--target=" ^ t; "--sysroot=" ^ sysroot;
|
||||||
|
"-resource-dir=" ^ wasm_resource_dir () ]
|
||||||
|
|
||||||
|
(* wasi-libc's start code calls __main_argc_argv, not main: clang *renames*
|
||||||
|
C's argc/argv [main] to that when it compiles C for wasm32, and the .ll
|
||||||
|
Emit writes says @main literally. Without this the link succeeds and the
|
||||||
|
program traps at its first instruction on a signature-mismatched weak stub.
|
||||||
|
The __asm__ label is load-bearing and must not be "simplified" away —
|
||||||
|
spelling the callee [main] makes clang rename *that* too, and the shim
|
||||||
|
becomes an infinite self-call that hangs rather than failing. *)
|
||||||
|
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"
|
||||||
|
|
||||||
(* What the compiler itself is, cheaply: its path, size and mtime. A clang
|
(* 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
|
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
|
[clang --version] subprocess on every build, which would cost most of what
|
||||||
@ -75,13 +243,17 @@ let clang_stamp =
|
|||||||
the source text, the compiler and the flags are all unchanged. The key has
|
the source text, the compiler and the flags are all unchanged. The key has
|
||||||
to carry [opt] and [target]: the acceptance table builds the same programs
|
to carry [opt] and [target]: the acceptance table builds the same programs
|
||||||
at -O0 and -O2, and an -O2 object must not serve an -O0 build. *)
|
at -O0 and -O2, and an -O2 object must not serve an -O0 build. *)
|
||||||
let compile_c ~opts ~src ~name =
|
let compile_c ~opts ?tflags ~src ~name () =
|
||||||
|
(* The whole flag list, not just the triple: on wasm32 the sysroot and the
|
||||||
|
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 key =
|
let key =
|
||||||
Digest.to_hex
|
Digest.to_hex
|
||||||
(Digest.string
|
(Digest.string
|
||||||
(String.concat "\000"
|
(String.concat "\000"
|
||||||
[ name; src; Lazy.force clang_stamp; opts.opt;
|
[ name; src; Lazy.force clang_stamp; opts.opt;
|
||||||
(match opts.target with None -> "" | Some t -> t) ]))
|
String.concat " " tflags ]))
|
||||||
in
|
in
|
||||||
let obj = Filename.concat (cachedir ()) (key ^ ".o") in
|
let obj = Filename.concat (cachedir ()) (key ^ ".o") in
|
||||||
if not (Sys.file_exists obj) then begin
|
if not (Sys.file_exists obj) then begin
|
||||||
@ -93,8 +265,7 @@ let compile_c ~opts ~src ~name =
|
|||||||
let tmp = Printf.sprintf "%s.%d.tmp" obj (Unix.getpid ()) in
|
let tmp = Printf.sprintf "%s.%d.tmp" obj (Unix.getpid ()) in
|
||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt; "-c" ]
|
([ Filename.quote clang; opts.opt; "-c" ] @ tflags
|
||||||
@ (match opts.target with None -> [] | Some t -> [ "--target=" ^ t ])
|
|
||||||
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
||||||
in
|
in
|
||||||
let code = Sys.command cmd in
|
let code = Sys.command cmd in
|
||||||
@ -105,18 +276,19 @@ let compile_c ~opts ~src ~name =
|
|||||||
end;
|
end;
|
||||||
obj
|
obj
|
||||||
|
|
||||||
let read_file 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
|
|
||||||
|
|
||||||
(* [csrcs] and [lflags] come from the imported packages (see [Load]): the C
|
(* [csrcs] and [lflags] come from the imported packages (see [Load]): the C
|
||||||
shim a package binds through, and the arguments needed to link the library
|
shim a package binds through, and the arguments needed to link the library
|
||||||
it binds to. *)
|
it binds to. *)
|
||||||
let executable ?(opts = default) ?(csrcs = []) ?(lflags = [])
|
let executable ?(opts = default) ?(csrcs = []) ?(lflags = [])
|
||||||
(p : Tast.program) ~out =
|
(p : Tast.program) ~out =
|
||||||
|
(* 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. *)
|
||||||
|
if wasm_target opts && opts.dev then
|
||||||
|
failwith
|
||||||
|
"wasm32: --dev is native only — the reload path is dlopen, which wasm32 \
|
||||||
|
has no equivalent of";
|
||||||
|
let tflags = target_flags opts in
|
||||||
let dir = workdir () in
|
let dir = workdir () in
|
||||||
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
||||||
write ll (Emit.program ~checks:opts.checks ~dev:opts.dev p);
|
write ll (Emit.program ~checks:opts.checks ~dev:opts.dev p);
|
||||||
@ -129,19 +301,19 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = [])
|
|||||||
rather than as a missing flag. The table is BSS, so this costs address
|
rather than as a missing flag. The table is BSS, so this costs address
|
||||||
space and not binary size, and [-rdynamic] and the cells are still what
|
space and not binary size, and [-rdynamic] and the cells are still what
|
||||||
[--dev] means. *)
|
[--dev] means. *)
|
||||||
|
let cc src name = compile_c ~opts ~tflags ~src ~name () in
|
||||||
let objs =
|
let objs =
|
||||||
compile_c ~opts ~src:Runtime_src.source ~name:"flan_rt.c"
|
cc Runtime_src.source "flan_rt.c"
|
||||||
:: [ compile_c ~opts ~src:Runtime_src.dev_source ~name:"flan_dev.c" ]
|
:: [ cc Runtime_src.dev_source "flan_dev.c" ]
|
||||||
@ List.map
|
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source]. *)
|
||||||
(fun c ->
|
@ (if wasm_target opts then [ cc wasm_main_source "flan_wasm_main.c" ] else [])
|
||||||
compile_c ~opts ~src:(read_file c) ~name:(Filename.basename c))
|
@ List.map (fun c -> cc (read_file c) (Filename.basename c)) csrcs
|
||||||
csrcs
|
|
||||||
in
|
in
|
||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
||||||
@ (if opts.dev then [ "-rdynamic" ] else [])
|
@ (if opts.dev then [ "-rdynamic" ] else [])
|
||||||
@ (match opts.target with None -> [] | Some t -> [ "--target=" ^ t ])
|
@ tflags
|
||||||
@ [ Filename.quote ll ]
|
@ [ Filename.quote ll ]
|
||||||
@ List.map Filename.quote objs
|
@ List.map Filename.quote objs
|
||||||
@ lflags
|
@ lflags
|
||||||
@ -186,6 +358,10 @@ let run what cmd =
|
|||||||
if code <> 0 then failwith (Printf.sprintf "%s failed (exit %d)" what code)
|
if code <> 0 then failwith (Printf.sprintf "%s failed (exit %d)" what code)
|
||||||
|
|
||||||
let shared ?(opts = default) ~ir ~out () : timing =
|
let shared ?(opts = default) ~ir ~out () : timing =
|
||||||
|
if wasm_target opts then
|
||||||
|
failwith
|
||||||
|
"wasm32: the reload path is native only — it is llc + ld -shared + \
|
||||||
|
dlopen, and wasm32 has no dlopen";
|
||||||
let dir = workdir () in
|
let dir = workdir () in
|
||||||
let base = Filename.remove_extension (Filename.basename out) in
|
let base = Filename.remove_extension (Filename.basename out) in
|
||||||
let ll = Filename.concat dir (base ^ ".ll") in
|
let ll = Filename.concat dir (base ^ ".ll") in
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user