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;
|
||||
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
|
||||
something is wrong with it. *)
|
||||
let workdir () =
|
||||
@ -53,6 +68,159 @@ type opts = {
|
||||
let default =
|
||||
{ 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
|
||||
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
|
||||
@ -75,13 +243,17 @@ let clang_stamp =
|
||||
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
|
||||
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 =
|
||||
Digest.to_hex
|
||||
(Digest.string
|
||||
(String.concat "\000"
|
||||
[ name; src; Lazy.force clang_stamp; opts.opt;
|
||||
(match opts.target with None -> "" | Some t -> t) ]))
|
||||
String.concat " " tflags ]))
|
||||
in
|
||||
let obj = Filename.concat (cachedir ()) (key ^ ".o") in
|
||||
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 cmd =
|
||||
String.concat " "
|
||||
([ Filename.quote clang; opts.opt; "-c" ]
|
||||
@ (match opts.target with None -> [] | Some t -> [ "--target=" ^ t ])
|
||||
([ Filename.quote clang; opts.opt; "-c" ] @ tflags
|
||||
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
||||
in
|
||||
let code = Sys.command cmd in
|
||||
@ -105,18 +276,19 @@ let compile_c ~opts ~src ~name =
|
||||
end;
|
||||
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
|
||||
shim a package binds through, and the arguments needed to link the library
|
||||
it binds to. *)
|
||||
let executable ?(opts = default) ?(csrcs = []) ?(lflags = [])
|
||||
(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 ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
||||
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
|
||||
space and not binary size, and [-rdynamic] and the cells are still what
|
||||
[--dev] means. *)
|
||||
let cc src name = compile_c ~opts ~tflags ~src ~name () in
|
||||
let objs =
|
||||
compile_c ~opts ~src:Runtime_src.source ~name:"flan_rt.c"
|
||||
:: [ compile_c ~opts ~src:Runtime_src.dev_source ~name:"flan_dev.c" ]
|
||||
@ List.map
|
||||
(fun c ->
|
||||
compile_c ~opts ~src:(read_file c) ~name:(Filename.basename c))
|
||||
csrcs
|
||||
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 [])
|
||||
@ List.map (fun c -> cc (read_file c) (Filename.basename c)) csrcs
|
||||
in
|
||||
let cmd =
|
||||
String.concat " "
|
||||
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
||||
@ (if opts.dev then [ "-rdynamic" ] else [])
|
||||
@ (match opts.target with None -> [] | Some t -> [ "--target=" ^ t ])
|
||||
@ tflags
|
||||
@ [ Filename.quote ll ]
|
||||
@ List.map Filename.quote objs
|
||||
@ lflags
|
||||
@ -186,6 +358,10 @@ let run what cmd =
|
||||
if code <> 0 then failwith (Printf.sprintf "%s failed (exit %d)" what code)
|
||||
|
||||
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 base = Filename.remove_extension (Filename.basename out) in
|
||||
let ll = Filename.concat dir (base ^ ".ll") in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user