(** {2 Command line} *) let concurrency, verbose, debug, secondary, force_byte_compilation, static, build_dir = let build_dir = ref "_boot" in let anon s = raise (Arg.Bad (Printf.sprintf "don't know what to do with %s\n" s)) in let concurrency = ref None in let verbose = ref false in let prog = Filename.basename Sys.argv.(0) in let debug = ref false in let secondary = ref false in let force_byte_compilation = ref false in let static = ref false in Arg.parse [ "-j", Int (fun n -> concurrency := Some n), "JOBS Concurrency" ; "--verbose", Set verbose, " Set the display mode" ; "--keep-generated-files", Unit ignore, " Keep generated files" ; "--debug", Set debug, " Enable various debugging options" ; "--secondary", Set secondary, " Use the secondary compiler installation" ; ( "--force-byte-compilation" , Set force_byte_compilation , " Force bytecode compilation even if ocamlopt is available" ) ; "--static", Set static, " Build a static binary" ; "--boot-dir", Set_string build_dir, " Set the boot directory" ] anon (Printf.sprintf "Usage: %s \nOptions are:" prog); !concurrency, !verbose, !debug, !secondary, !force_byte_compilation, !static, !build_dir ;; (** {2 General configuration} *) type task = { target : string * string ; external_libraries : string list ; local_libraries : Libs.library list } let task = { target = "dune", "bin/main.ml" ; external_libraries = Libs.external_libraries ; local_libraries = Libs.local_libraries } ;; (** {2 Utility functions} *) open StdLabels open Printf module String = struct include String module Set = Set.Make (String) module Map = Map.Make (String) let is_suffix t ~suffix = Filename.check_suffix t suffix let is_prefix t ~prefix = let len_s = length t and len_pre = length prefix in let rec aux i = if i = len_pre then true else if unsafe_get t i <> unsafe_get prefix i then false else aux (i + 1) in len_s >= len_pre && aux 0 ;; end module List = struct include List let partition_map_skip t ~f = let rec loop l m r = function | [] -> l, m, r | x :: xs -> (match f x with | `Skip -> loop l m r xs | `Left x -> loop (x :: l) m r xs | `Middle x -> loop l (x :: m) r xs | `Right x -> loop l m (x :: r) xs) in let l, m, r = loop [] [] [] t in rev l, rev m, rev r ;; let rec filter_map l ~f = match l with | [] -> [] | x :: l -> (match f x with | None -> filter_map l ~f | Some x -> x :: filter_map l ~f) ;; end let ( ^/ ) = Filename.concat let fatal fmt = ksprintf (fun s -> prerr_endline s; exit 2) fmt ;; module Status_line = struct let num_jobs = ref 0 let num_jobs_finished = ref 0 let displayed = ref "" let display_status_line = Unix.(isatty stdout) || match Sys.getenv "INSIDE_EMACS" with | (_ : string) -> true | exception Not_found -> false ;; let update jobs = if display_status_line && !num_jobs > 0 then ( let new_displayed = sprintf "Done: %d/%d (jobs: %d)" !num_jobs_finished !num_jobs jobs in Printf.printf "\r%*s\r%s%!" (String.length !displayed) "" new_displayed; displayed := new_displayed) ;; let clear () = Printf.printf "\r*s\r%!" let () = at_exit (fun () -> Printf.printf "\r%*s\r" (String.length !displayed) "") end (* Return a sorted list of entries in [path] as [path/entry] *) let readdir path = Sys.readdir path |> Array.to_list |> List.map ~f:(fun entry -> path ^/ entry) |> List.sort ~cmp:String.compare ;; let open_out file = if Sys.file_exists file then fatal "%s already exists" file; open_out file ;; let input_lines ic = let rec loop ic acc = match input_line ic with | line -> loop ic (line :: acc) | exception End_of_file -> List.rev acc in loop ic [] ;; let read_lines fn = let ic = open_in fn in let lines = input_lines ic in close_in ic; lines ;; let read_file fn = let ic = open_in_bin fn in let s = really_input_string ic (in_channel_length ic) in close_in ic; s ;; let split_lines s = let rec loop ~last_is_cr ~acc i j = if j = String.length s then ( let acc = if j = i || (j = i + 1 && last_is_cr) then acc else String.sub s ~pos:i ~len:(j - i) :: acc in List.rev acc) else ( match s.[j] with | '\r' -> loop ~last_is_cr:true ~acc i (j + 1) | '\n' -> let line = let len = if last_is_cr then j - i - 1 else j - i in String.sub s ~pos:i ~len in loop ~acc:(line :: acc) (j + 1) (j + 1) ~last_is_cr:false | _ -> loop ~acc i (j + 1) ~last_is_cr:false) in loop ~acc:[] 0 0 ~last_is_cr:false ;; let do_then_copy ~f a b = if Sys.file_exists b then fatal "%s already exists" b; let ic = open_in_bin a in let len = in_channel_length ic in let s = really_input_string ic len in close_in ic; let oc = open_out_bin b in f oc; output_string oc s; close_out oc ;; (* copy a file - fails if the file exists *) let copy a b = do_then_copy ~f:(fun _ -> ()) a b (* copy a file and insert a header - fails if the file exists *) let copy_with_header ~header a b = do_then_copy ~f:(fun oc -> output_string oc header) a b (* copy a file and insert a directive - fails if the file exists *) let copy_with_directive ~directive a b = do_then_copy ~f:(fun oc -> fprintf oc "#%s 1 %S\n" directive a) a b ;; let path_sep = if Sys.win32 then ';' else ':' let split_path s = let rec loop i j = if j = String.length s then [ String.sub s ~pos:i ~len:(j - i) ] else if s.[j] = path_sep then String.sub s ~pos:i ~len:(j - i) :: loop (j + 1) (j + 1) else loop i (j + 1) in loop 0 0 ;; let path = match Sys.getenv "PATH" with | exception Not_found -> [] | s -> split_path s ;; let find_prog ~f = let rec search = function | [] -> None | dir :: rest -> (match f dir with | None -> search rest | Some fn -> Some (dir, fn)) in search path ;; let exe = if Sys.win32 then ".exe" else "" (** {2 Concurrency level} *) let concurrency = let try_run_and_capture_line (prog, args) = match find_prog ~f:(fun dir -> if Sys.file_exists (dir ^/ prog) then Some prog else None) with | None -> None | Some (dir, prog) -> let path = dir ^/ prog in let args = Array.of_list @@ (path :: args) in let ic, oc, ec = Unix.open_process_args_full path args (Unix.environment ()) in let line = match input_line ic with | s -> Some s | exception End_of_file -> None in (match Unix.close_process_full (ic, oc, ec), line with | WEXITED 0, Some s -> Some s | _ -> None) in match concurrency with | Some n -> n | None -> (* If no [-j] was given, try to autodetect the number of processors *) if Sys.win32 then ( match Sys.getenv_opt "NUMBER_OF_PROCESSORS" with | None -> 1 | Some s -> (match int_of_string s with | exception _ -> 1 | n -> n)) else ( let commands = [ "nproc", [] ; "getconf", [ "_NPROCESSORS_ONLN" ] ; "getconf", [ "NPROCESSORS_ONLN" ] ] in let rec loop = function | [] -> 1 | cmd :: rest -> (match try_run_and_capture_line cmd with | None -> loop rest | Some s -> (match int_of_string (String.trim s) with | n -> n | exception _ -> loop rest)) in loop commands) ;; (** {2 Fibers} *) module Fiber : sig (** Fibers *) (** This module is similar to the one in [../src/fiber] except that it is much less optimised and much easier to understand. You should look at the documentation of the other module to understand the API. *) type 'a t val return : 'a -> 'a t module O : sig val ( >>> ) : unit t -> 'a t -> 'a t val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t end module Future : sig type 'a fiber type 'a t val wait : 'a t -> 'a fiber end with type 'a fiber := 'a t val fork : (unit -> 'a t) -> 'a Future.t t val fork_and_join : (unit -> 'a t) -> (unit -> 'b t) -> ('a * 'b) t val fork_and_join_unit : (unit -> unit t) -> (unit -> 'a t) -> 'a t val parallel_map : 'a list -> f:('a -> 'b t) -> 'b list t val parallel_iter : 'a list -> f:('a -> unit t) -> unit t module Process : sig val run : ?cwd:string -> string -> string list -> unit t val run_and_capture : ?cwd:string -> string -> string list -> string t val try_run_and_capture : ?cwd:string -> string -> string list -> string option t end val run : 'a t -> 'a end = struct open MoreLabels type 'a t = ('a -> unit) -> unit let return x k = k x module O = struct let ( >>> ) a b k = a (fun () -> b k) let ( >>= ) t f k = t (fun x -> f x k) let ( >>| ) t f k = t (fun x -> k (f x)) end open O let both a b = a >>= fun a -> b >>= fun b -> return (a, b) module Ivar = struct type 'a state = | Full of 'a | Empty of ('a -> unit) Queue.t type 'a t = { mutable state : 'a state } let create () = { state = Empty (Queue.create ()) } let fill t x = match t.state with | Full _ -> failwith "Fiber.Ivar.fill" | Empty q -> t.state <- Full x; Queue.iter (fun f -> f x) q ;; let read t k = match t.state with | Full x -> k x | Empty q -> Queue.push k q ;; end module Future = struct type 'a t = 'a Ivar.t let wait = Ivar.read end let fork f k = let ivar = Ivar.create () in f () (fun x -> Ivar.fill ivar x); k ivar ;; let fork_and_join f g = fork f >>= fun a -> fork g >>= fun b -> both (Future.wait a) (Future.wait b) ;; let fork_and_join_unit f g = fork f >>= fun a -> fork g >>= fun b -> Future.wait a >>> Future.wait b ;; let rec parallel_map l ~f = match l with | [] -> return [] | x :: l -> fork (fun () -> f x) >>= fun future -> parallel_map l ~f >>= fun l -> Future.wait future >>= fun x -> return (x :: l) ;; let rec parallel_iter l ~f = match l with | [] -> return () | x :: l -> fork (fun () -> f x) >>= fun future -> parallel_iter l ~f >>= fun () -> Future.wait future ;; module Temp = struct module Files = Set.Make (String) let tmp_files = ref Files.empty let () = at_exit (fun () -> let fns = !tmp_files in tmp_files := Files.empty; Files.iter fns ~f:(fun fn -> try Sys.remove fn with | _ -> ())) ;; let file prefix suffix = let fn = Filename.temp_file prefix suffix in tmp_files := Files.add fn !tmp_files; fn ;; let destroy_file fn = (try Sys.remove fn with | _ -> ()); tmp_files := Files.remove fn !tmp_files ;; end module Process = struct let running = Hashtbl.create concurrency exception Finished of int * Unix.process_status let rec wait_win32 () = match Hashtbl.iter running ~f:(fun ~key:pid ~data:_ -> let pid, status = Unix.waitpid [ WNOHANG ] pid in if pid <> 0 then raise_notrace (Finished (pid, status))) with | () -> ignore (Unix.select [] [] [] 0.001); wait_win32 () | exception Finished (pid, status) -> pid, status ;; let wait = if Sys.win32 then wait_win32 else Unix.wait let waiting_for_slot = Queue.create () let throttle () = if Hashtbl.length running >= concurrency then ( let ivar = Ivar.create () in Queue.push ivar waiting_for_slot; Ivar.read ivar) else return () ;; let restart_throttled () = while Hashtbl.length running < concurrency && not (Queue.is_empty waiting_for_slot) do Ivar.fill (Queue.pop waiting_for_slot) () done ;; let open_temp_file () = let out = Temp.file "duneboot-" ".output" in let fd = Unix.openfile out [ O_WRONLY; O_CREAT; O_TRUNC; O_SHARE_DELETE; O_CLOEXEC ] 0o666 in out, fd ;; let read_temp fn = let s = read_file fn in Temp.destroy_file fn; s ;; let initial_cwd = Sys.getcwd () let run_process ?cwd prog args ~split = throttle () >>= fun () -> let stdout_fn, stdout_fd = open_temp_file () in let stderr_fn, stderr_fd = if split then open_temp_file () else stdout_fn, stdout_fd in (match cwd with | Some x -> Sys.chdir x | None -> ()); let pid = Unix.create_process prog (Array.of_list (prog :: args)) Unix.stdin stdout_fd stderr_fd in (match cwd with | Some _ -> Sys.chdir initial_cwd | None -> ()); Unix.close stdout_fd; if split then Unix.close stderr_fd; let ivar = Ivar.create () in Hashtbl.add running ~key:pid ~data:ivar; Ivar.read ivar >>= fun (status : Unix.process_status) -> let stdout_s = read_temp stdout_fn in let stderr_s = if split then read_temp stderr_fn else stdout_s in if stderr_s <> "" || status <> WEXITED 0 || verbose then ( let cmdline = String.concat ~sep:" " (prog :: args) in let cmdline = match cwd with | Some x -> sprintf "cd %s && %s" x cmdline | None -> cmdline in Status_line.clear (); prerr_endline cmdline; prerr_string stderr_s; flush stderr); match status with | WEXITED 0 -> return (Ok stdout_s) | WEXITED n -> return (Error n) | WSIGNALED _ -> return (Error 255) | WSTOPPED _ -> assert false ;; let run ?cwd prog args = run_process ?cwd prog args ~split:false >>| function | Ok _ -> () | Error n -> exit n ;; let run_and_capture ?cwd prog args = run_process ?cwd prog args ~split:true >>| function | Ok x -> x | Error n -> exit n ;; let try_run_and_capture ?cwd prog args = run_process ?cwd prog args ~split:true >>| function | Ok x -> Some x | Error _ -> None ;; end let run t = let result = ref None in t (fun x -> result := Some x); let rec loop () = if Hashtbl.length Process.running > 0 then ( Status_line.update (Hashtbl.length Process.running); let pid, status = Process.wait () in let ivar = Hashtbl.find Process.running pid in Hashtbl.remove Process.running pid; Ivar.fill ivar status; Process.restart_throttled (); loop ()) else ( match !result with | Some x -> x | None -> fatal "bootstrap got stuck!") in loop () ;; end open Fiber.O module Process = Fiber.Process (** {2 OCaml tools} *) module Mode = struct type t = | Byte | Native end module Config : sig val compiler : string val ocamldep : string val ocamllex : string val ocamlyacc : string val mode : Mode.t val ocaml_archive_ext : string val ocaml_config : unit -> string String.Map.t Fiber.t val output_complete_obj_arg : string val unix_library_flags : string list end = struct let ocaml_version = Scanf.sscanf Sys.ocaml_version "%d.%d" (fun a b -> a, b) let prog_not_found prog = fatal "Program %s not found in PATH" prog let best_prog dir prog = let fn = dir ^/ prog ^ ".opt" ^ exe in if Sys.file_exists fn then Some fn else ( let fn = dir ^/ prog ^ exe in if Sys.file_exists fn then Some fn else None) ;; let find_prog prog = find_prog ~f:(fun dir -> best_prog dir prog) let get_prog dir prog = match best_prog dir prog with | None -> prog_not_found prog | Some fn -> fn ;; let bin_dir, ocamlc = if secondary then ( let s = Fiber.run (Process.run_and_capture "ocamlfind" [ "-toolchain"; "secondary"; "query"; "ocaml" ]) in match split_lines s with | [] | _ :: _ :: _ -> fatal "Unexpected output locating secondary compiler" | [ bin_dir ] -> (match best_prog bin_dir "ocamlc" with | None -> fatal "Failed to locate secondary ocamlc" | Some x -> bin_dir, x)) else ( match find_prog "ocamlc" with | None -> prog_not_found "ocamlc" | Some x -> x) ;; let ocamlyacc = get_prog bin_dir "ocamlyacc" let ocamllex = get_prog bin_dir "ocamllex" let ocamldep = get_prog bin_dir "ocamldep" let compiler, mode, ocaml_archive_ext = match force_byte_compilation, best_prog bin_dir "ocamlopt" with | true, _ | _, None -> ocamlc, Mode.Byte, ".cma" | false, Some path -> path, Mode.Native, ".cmxa" ;; let ocaml_config () = Process.run_and_capture ocamlc [ "-config" ] >>| fun s -> List.fold_left (split_lines s) ~init:String.Map.empty ~f:(fun acc line -> match Scanf.sscanf line "%[^:]: %s" (fun k v -> k, v) with | k, v -> String.Map.add k v acc | exception _ -> fatal "invalid line in output of 'ocamlc -config': %s" (String.escaped line)) ;; let output_complete_obj_arg = if ocaml_version < (4, 10) then "-custom" else "-output-complete-exe" ;; let unix_library_flags = if ocaml_version >= (5, 0) then [ "-I"; "+unix" ] else [] end let insert_header fn ~header = match header with | "" -> () | h -> let s = read_file fn in let oc = open_out_bin fn in output_string oc h; output_string oc s; close_out oc ;; let copy_lexer ~header src dst = let dst = Filename.remove_extension dst ^ ".ml" in Process.run Config.ocamllex [ "-q"; "-o"; dst; src ] >>| fun () -> insert_header dst ~header ;; let copy_parser ~header src dst = let dst = Filename.remove_extension dst in Process.run Config.ocamlyacc [ "-b"; dst; src ] >>| fun () -> insert_header (dst ^ ".ml") ~header; insert_header (dst ^ ".mli") ~header ;; (** {2 Handling of the dune-build-info library} *) (** {2 Preparation of library files} *) module Build_info = struct let get_version () = let from_dune_project = match read_lines "dune-project" with | exception _ -> None | lines -> let rec loop = function | [] -> None | line :: lines -> (match Scanf.sscanf line "(version %[^)])" (fun v -> v) with | exception _ -> loop lines | v -> Some v) in loop lines in match from_dune_project with | Some _ -> Fiber.return from_dune_project | None -> if not (Sys.file_exists ".git") then Fiber.return None else Process.try_run_and_capture "git" [ "describe"; "--always"; "--dirty"; "--abbrev=7" ] >>| (function | Some s -> Some (String.trim s) | None -> None) ;; let gen_data_module oc = let pr fmt = fprintf oc fmt in let prlist name l ~f = match l with | [] -> pr "let %s = []\n" name | x :: l -> pr "let %s =\n" name; pr " [ "; f x; List.iter l ~f:(fun x -> pr " ; "; f x); pr " ]\n" in get_version () >>| fun version -> pr "let version = %s\n" (match version with | None -> "None" | Some v -> sprintf "Some %S" v); pr "\n"; let libs = List.map task.local_libraries ~f:(fun (lib : Libs.library) -> lib.path, "version") @ List.map task.external_libraries ~f:(fun name -> name, {|Some "[distributed with OCaml]"|}) |> List.sort ~cmp:(fun (a, _) (b, _) -> String.compare a b) in prlist "statically_linked_libraries" libs ~f:(fun (name, v) -> pr "%S, %s\n" name v) ;; end (* module OCaml_file = struct module Kind = struct type t = Impl | Intf end type t = { kind : Kind.t ; module_name = end *) module Library = struct module File_kind = struct type asm = { syntax : [ `Gas | `Intel ] ; arch : [ `Amd64 ] option ; os : [ `Win | `Unix ] option ; assembler : [ `C_comp | `Msvc_asm ] } type c = { arch : [ `Arm64 | `X86 ] option ; flags : string list } type t = | Header | C of c | Asm of asm | Ml | Mli | Mll | Mly let analyse file = let fn = Filename.basename file in let i = try String.index fn '.' with | Not_found -> String.length fn in match String.sub fn ~pos:i ~len:(String.length fn - i) with | (".S" | ".asm") as ext -> let syntax = if ext = ".S" then `Gas else `Intel in let os, arch, assembler = let fn = Filename.remove_extension fn in let check suffix = String.is_suffix fn ~suffix in if check "x86-64_unix" then Some `Unix, Some `Amd64, `C_comp else if check "x86-64_windows_gnu" then Some `Win, Some `Amd64, `C_comp else if check "x86-64_windows_msvc" then Some `Win, Some `Amd64, `Msvc_asm else None, None, `C_comp in Some (Asm { syntax; arch; os; assembler }) | ".c" -> let arch, flags = let fn = Filename.remove_extension fn in let check suffix = String.is_suffix fn ~suffix in let x86 gnu _msvc = (* CR rgrinberg: select msvc flags on windows *) Some `X86, gnu in if check "_sse2" then x86 [ "-msse2" ] [ "/arch:SSE2" ] else if check "_sse41" then x86 [ "-msse4.1" ] [ "/arch:AVX" ] else if check "_avx2" then x86 [ "-mavx2" ] [ "/arch:AVX2" ] else if check "_avx512" then x86 [ "-mavx512f"; "-mavx512vl"; "-mavx512bw" ] [ "/arch:AVX512" ] else if String.is_suffix fn ~suffix:"_neon" then Some `Arm64, [] else None, [] in Some (C { arch; flags }) | ".h" -> Some Header | ".ml" -> Some Ml | ".mli" -> Some Mli | ".mll" -> Some Mll | ".mly" -> Some Mly | ".defaults.ml" -> let fn' = String.sub fn ~pos:0 ~len:i ^ ".ml" in let dn = Filename.dirname file in if Sys.file_exists (dn ^/ fn') then None else Some Ml | _ -> None ;; end type source = { file : string ; kind : File_kind.t } type c_file = { name : string ; flags : string list } type asm_file = { assembler : [ `C_comp | `Msvc_asm ] ; flags : string list ; out_file : string } module Wrapper = struct type t = { toplevel_module : string ; alias_module : string } let make ~namespace ~modules = match namespace with | None -> None | Some namespace -> let namespace = String.capitalize_ascii namespace in if String.Set.equal modules (String.Set.singleton namespace) then None else if String.Set.mem namespace modules then Some { toplevel_module = namespace; alias_module = namespace ^ "__" } else Some { toplevel_module = namespace; alias_module = namespace } ;; let mangle_filename t ({ file; kind } : source) = let base = Filename.basename file in match kind with | Asm _ | C _ | Header -> base | Mll | Mly | Ml | Mli -> let ext = match kind with | Mli -> ".mli" | _ -> ".ml" in let base = String.sub base ~pos:0 ~len:(String.index base '.') |> String.uncapitalize_ascii in (match t with | None -> base ^ ext | Some t -> if String.capitalize_ascii base = t.toplevel_module then base ^ ext else ( let base = String.capitalize_ascii base in String.uncapitalize_ascii t.toplevel_module ^ "__" ^ base ^ ext)) ;; let header t = match t with | None -> "" | Some t -> sprintf "open! %s\n" t.alias_module ;; let generate_wrapper t modules = match t with | None -> None | Some t -> let fn = String.uncapitalize_ascii t.alias_module ^ ".ml" in let oc = open_out (build_dir ^/ fn) in String.Set.iter (fun m -> if m <> t.toplevel_module then fprintf oc "module %s = %s__%s\n" m t.toplevel_module m) modules; close_out oc; Some fn ;; end (* Collect source files *) let scan ~dir ~scan_subdirs = let rec loop files acc = match files with | [] -> acc | file :: files -> let acc = if Sys.is_directory file then if scan_subdirs then loop (readdir file) acc else acc else ( match File_kind.analyse file with | Some kind -> { file; kind } :: acc | None -> acc) in loop files acc in loop (readdir dir) [] ;; type t = { ocaml_files : string list ; alias_file : string option ; c_files : c_file list ; asm_files : asm_file list } let keep_asm { File_kind.syntax; arch; os; assembler = _ } ~ccomp_type ~architecture ~os_type = (match os with | Some `Unix -> String.equal os_type "Unix" | Some `Win -> String.equal os_type "Win32" | None -> true) && (match syntax, ccomp_type with | `Intel, "msvc" -> true | `Gas, "msvc" -> false | `Gas, _ -> true | `Intel, _ -> false) && match arch, architecture with | None, _ -> true | Some `Amd64, "amd64" -> true | Some `Amd64, _ -> false ;; let keep_c { File_kind.arch; flags = _ } ~architecture = match arch with | None -> true | Some `Arm64 -> architecture = "arm64" | Some `X86 -> architecture = "amd64" || architecture = "x86_64" ;; let process { Libs.path = dir ; main_module_name = namespace ; include_subdirs_unqualified = scan_subdirs ; special_builtin_support = build_info_module } ~ocaml_config ~word_size ~os_type = let files = scan ~dir ~scan_subdirs in let modules = let modules = List.fold_left files ~init:String.Set.empty ~f:(fun acc { file = fn; kind } -> match (kind : File_kind.t) with | Asm _ | Header | C _ -> acc | Ml | Mli | Mll | Mly -> let module_name = let fn = Filename.basename fn in String.sub fn ~pos:0 ~len:(String.index fn '.') |> String.capitalize_ascii in String.Set.add module_name acc) in match build_info_module with | None -> modules | Some m -> String.Set.add (String.capitalize_ascii m) modules in let wrapper = Wrapper.make ~namespace ~modules in let header = Wrapper.header wrapper in Fiber.fork_and_join (fun () -> Fiber.parallel_map files ~f:(fun ({ file = fn; kind } as source) -> let mangled = Wrapper.mangle_filename wrapper source in let dst = build_dir ^/ mangled in (match kind with | Asm _ -> copy fn dst; Fiber.return [ mangled ] | Header | C _ -> copy_with_directive ~directive:"line" fn dst; Fiber.return [ mangled ] | Ml | Mli -> copy_with_header ~header fn dst; Fiber.return [ mangled ] | Mll -> copy_lexer fn dst ~header >>> Fiber.return [ mangled ] | Mly -> (* CR rgrinberg: what if the parser already has an mli? *) copy_parser fn dst ~header >>> Fiber.return [ mangled; mangled ^ "i" ]) >>| function | mangled -> List.map mangled ~f:(fun m -> source, m))) (fun () -> match build_info_module with | None -> Fiber.return None | Some m -> let src = let fn = String.uncapitalize_ascii m ^ ".ml" in { file = fn; kind = Ml } in let mangled = Wrapper.mangle_filename wrapper src in let oc = open_out (build_dir ^/ mangled) in Build_info.gen_data_module oc >>| fun () -> close_out oc; Some (src, mangled)) >>| fun (files, build_info_file) -> let alias_file = Wrapper.generate_wrapper wrapper modules in let c_files, ocaml_files, asm_files = let files = let files = List.concat files in match build_info_file with | None -> files | Some fn -> fn :: files in let ext_obj = try String.Map.find "ext_obj" ocaml_config with | Not_found -> ".o" in let ccomp_type = String.Map.find "ccomp_type" ocaml_config in let architecture = String.Map.find "architecture" ocaml_config in List.partition_map_skip files ~f:(fun (src, fn) -> match src.kind with | C c -> if keep_c c ~architecture then ( let extra_flags = if String.is_prefix ~prefix:"blake3_" fn then if String.equal os_type "Cygwin" || String.equal word_size "32" then [ "-DBLAKE3_NO_SSE2" ; "-DBLAKE3_NO_SSE41" ; "-DBLAKE3_NO_AVX2" ; "-DBLAKE3_NO_AVX512" ] else [] else [] in `Left { flags = extra_flags @ c.flags; name = fn }) else `Skip | Ml | Mli | Mly | Mll -> `Middle fn | Header -> `Skip | Asm asm -> if keep_asm asm ~ccomp_type ~architecture ~os_type then ( let out_file = Filename.chop_extension fn ^ ext_obj in `Right { flags = (match asm.assembler with | `C_comp -> [ "-c"; fn; "-o"; out_file ] | `Msvc_asm -> [ "/nologo"; "/quiet"; "/Fo" ^ out_file; "/c"; fn ]) ; assembler = asm.assembler ; out_file }) else `Skip) in { ocaml_files; alias_file; c_files; asm_files } ;; end let ocamldep args = Process.run_and_capture Config.ocamldep ("-modules" :: args) ~cwd:build_dir >>| fun s -> List.map (split_lines s) ~f:(fun line -> let colon = String.index line ':' in let filename = String.sub line ~pos:0 ~len:colon in let modules = if colon = String.length line - 1 then [] else ( let modules = String.sub line ~pos:(colon + 2) ~len:(String.length line - colon - 2) in String.split_on_char ~sep:' ' modules) in filename, modules) |> List.sort ~cmp:compare ;; let mk_flags arg l = List.map l ~f:(fun m -> [ arg; m ]) |> List.flatten let convert_dependencies ~all_source_files (file, dependencies) = let is_mli = Filename.check_suffix file ".mli" in let convert_module module_name = let filename = String.uncapitalize_ascii module_name in if filename = Filename.chop_extension file then (* Self-reference *) None else if String.Set.mem (filename ^ ".mli") all_source_files then if (not is_mli) && String.Set.mem (filename ^ ".ml") all_source_files then (* We need to build the .ml for inlining info *) Some [ filename ^ ".mli"; filename ^ ".ml" ] else (* .mli files never depend on .ml files *) Some [ filename ^ ".mli" ] else if String.Set.mem (filename ^ ".ml") all_source_files then (* If there's no .mli, then we must always depend on the .ml *) Some [ filename ^ ".ml" ] else (* This is a module coming from an external library *) None in let dependencies = let dependencies = List.concat (List.filter_map ~f:convert_module dependencies) in (* .ml depends on .mli, if it exists *) if (not is_mli) && String.Set.mem (file ^ "i") all_source_files then (file ^ "i") :: dependencies else dependencies in file, dependencies ;; let write_args file args = let ch = open_out (build_dir ^/ file) in output_string ch (String.concat ~sep:"\n" args); close_out ch ;; let get_dependencies libraries = let alias_files = List.fold_left libraries ~init:[] ~f:(fun acc (lib : Library.t) -> match lib.alias_file with | None -> acc | Some fn -> fn :: acc) in let all_source_files = List.map ~f:(fun (lib : Library.t) -> lib.ocaml_files) libraries |> List.concat in write_args "source_files" all_source_files; ocamldep (mk_flags "-map" alias_files @ [ "-args"; "source_files" ]) >>| fun dependencies -> let all_source_files = List.fold_left alias_files ~init:(String.Set.of_list all_source_files) ~f:(fun acc fn -> String.Set.add fn acc) in let deps = List.rev_append ((* Alias files have no dependencies *) List.rev_map alias_files ~f:(fun fn -> fn, [])) (List.rev_map dependencies ~f:(convert_dependencies ~all_source_files)) in if debug then ( eprintf "***** Dependencies *****\n"; List.iter deps ~f:(fun (fn, deps) -> eprintf "%s: %s\n" fn (String.concat deps ~sep:" ")); eprintf "**********\n"); deps ;; let assemble_libraries { local_libraries; target = _, main; _ } ~ocaml_config ~word_size ~os_type = (* In order to assemble all the sources in one place, the executables modules are also put in a namespace *) let task_lib = let dir = Filename.dirname main in let namespace = String.capitalize_ascii (Filename.chop_extension (Filename.basename main)) in { Libs.path = dir ; main_module_name = Some namespace ; include_subdirs_unqualified = true ; special_builtin_support = None } in local_libraries @ [ task_lib ] |> Fiber.parallel_map ~f:(Library.process ~ocaml_config ~word_size ~os_type) ;; type status = | Not_started of (unit -> unit Fiber.t) | Initializing | Started of unit Fiber.Future.t let resolve_externals external_libraries = let external_libraries, external_includes = let convert = function | "threads" -> "threads" ^ Config.ocaml_archive_ext, [ "-I"; "+threads" ] | "unix" -> "unix" ^ Config.ocaml_archive_ext, Config.unix_library_flags | s -> fatal "unhandled external library %s" s in List.map ~f:convert external_libraries |> List.split in let external_includes = List.concat external_includes in external_libraries, external_includes ;; let sort_files dependencies ~main = let deps_by_file = Hashtbl.create (List.length dependencies) in List.iter dependencies ~f:(fun (file, deps) -> Hashtbl.add deps_by_file file deps); let seen = ref String.Set.empty in let res = ref [] in let rec loop file = if not (String.Set.mem file !seen) then ( seen := String.Set.add file !seen; List.iter (Hashtbl.find deps_by_file file) ~f:loop; res := file :: !res) in loop (Filename.basename main); List.rev !res ;; let common_build_args name ~external_includes ~external_libraries = List.concat [ [ "-o"; name ^ ".exe"; "-g" ] ; (match Config.mode with | Byte -> [ Config.output_complete_obj_arg ] | Native -> []) ; external_includes ; external_libraries ] ;; let allow_unstable_sources = [ "-alert"; "-unstable" ] let ocaml_warnings = let warnings = [ (* Warning 49 [no-cmi-file]: no cmi file was found in path for module *) "-49" ; (* Warning 23: all the fields are explicitly listed in this record: the 'with' clause is useless. In order to stay version independent, we use a trick with `with` by creating a dummy value and filling in the fields available in every OCaml version. forced_major_collections is the one missing in versions older than 4.12. We therefore disable warning 23 for our purposes. *) "-23" ; (* Warning 53 [misplaced-attribute]: the "alert" attribute cannot appear in this context Any .mli files that begin wtih [@@@alert] will cause the compiler to emit warning 53 due to the way we alias modules with an `open!`. It may be possible to use the command line flag `-open` instead, however this complicates dependency tracking so disabling this warning instead is suitable for our purposes. *) "-53" ] in [ "-w"; String.concat ~sep:"" warnings ] ;; let build ~ocaml_config ~dependencies ~c_files ~asm_files ~build_flags ~link_flags { target = name, main; external_libraries; _ } = let c_compiler = String.Map.find "c_compiler" ocaml_config in let ext_obj = try String.Map.find "ext_obj" ocaml_config with | Not_found -> ".o" in let num_dependencies = List.length dependencies in let table = Hashtbl.create num_dependencies in Status_line.num_jobs := num_dependencies; let build m = match Hashtbl.find table m with | Not_started f -> Hashtbl.replace table m Initializing; Fiber.fork f >>= fun fut -> Hashtbl.replace table m (Started fut); Fiber.Future.wait fut >>| fun () -> incr Status_line.num_jobs_finished | Initializing -> fatal "dependency cycle!" | Started fut -> Fiber.Future.wait fut | exception Not_found -> fatal "file not found: %s" m in let external_libraries, external_includes = resolve_externals external_libraries in List.iter dependencies ~f:(fun (file, deps) -> Hashtbl.add table file (Not_started (fun () -> Fiber.parallel_iter deps ~f:build >>= fun () -> Process.run ~cwd:build_dir Config.compiler (List.concat [ [ "-c"; "-g"; "-no-alias-deps" ] ; ocaml_warnings ; allow_unstable_sources ; external_includes ; [ file ] ])))); Fiber.fork_and_join_unit (fun () -> build (Filename.basename main)) (fun () -> (Fiber.fork_and_join (fun () -> Fiber.parallel_map c_files ~f:(fun { Library.name = file; flags } -> let flags = List.map flags ~f:(fun flag -> [ "-ccopt"; flag ]) |> List.concat in Process.run ~cwd:build_dir Config.compiler (List.concat [ [ "-c"; "-g" ]; external_includes; build_flags; [ file ]; flags ]) >>| fun () -> Filename.chop_extension file ^ ext_obj))) (fun () -> Fiber.parallel_map asm_files ~f:(fun { Library.assembler; flags; out_file } -> Process.run ~cwd:build_dir (match assembler with | `C_comp -> c_compiler | `Msvc_asm -> "ml64.exe") flags >>| fun () -> out_file)) >>| fun (x, y) -> x @ y) >>= fun obj_files -> let compiled_ml_files = let compiled_ml_ext = match Config.mode with | Byte -> ".cmo" | Native -> ".cmx" in List.filter_map (sort_files dependencies ~main) ~f:(fun fn -> match Filename.extension fn with | ".ml" -> Some (Filename.remove_extension fn ^ compiled_ml_ext) | _ -> None) in write_args "compiled_ml_files" compiled_ml_files; Process.run ~cwd:build_dir Config.compiler (let static_flags = if static then [ "-ccopt"; "-static" ] else [] in List.concat [ common_build_args name ~external_includes ~external_libraries ; obj_files ; [ "-args"; "compiled_ml_files" ] ; link_flags ; static_flags ; allow_unstable_sources ]) ;; let rec rm_rf fn = match Unix.lstat fn with | { st_kind = S_DIR; _ } -> clear fn; Unix.rmdir fn | _ -> Unix.unlink fn | exception Unix.Unix_error (ENOENT, _, _) -> () and clear dir = List.iter (readdir dir) ~f:rm_rf let rec get_flags system = function | (set, f) :: r -> if List.mem system ~set then f else get_flags system r | [] -> [] ;; (** {2 Bootstrap process} *) let main () = (try clear build_dir with | Sys_error _ -> ()); (try Unix.mkdir build_dir 0o777 with | Unix.Unix_error (Unix.EEXIST, _, _) -> ()); Config.ocaml_config () >>= fun ocaml_config -> let word_size = String.Map.find "word_size" ocaml_config in let os_type = String.Map.find "os_type" ocaml_config in assemble_libraries ~ocaml_config ~word_size ~os_type task >>= fun libraries -> let c_files = List.map ~f:(fun (lib : Library.t) -> lib.c_files) libraries |> List.concat in let asm_files = List.map ~f:(fun (lib : Library.t) -> lib.asm_files) libraries |> List.concat in get_dependencies libraries >>= fun dependencies -> let ocaml_system = match String.Map.find_opt "system" ocaml_config with | None -> assert false | Some s -> s in let build_flags = get_flags ocaml_system Libs.build_flags in let link_flags = get_flags ocaml_system Libs.link_flags in build ~ocaml_config ~dependencies ~asm_files ~c_files ~build_flags ~link_flags task ;; let () = Fiber.run (main ())