dune/src/gen_rules.ml

1154 lines
42 KiB
OCaml

open Import
open Jbuild
open Build.O
open! No_io
(* +-----------------------------------------------------------------+
| Utils |
+-----------------------------------------------------------------+ *)
module type Params = sig
val sctx : Super_context.t
end
module Gen(P : Params) = struct
module SC = Super_context
open P
let ctx = SC.context sctx
(* +-----------------------------------------------------------------+
| Interpretation of [modules] fields |
+-----------------------------------------------------------------+ *)
let parse_modules ~dir ~all_modules ~modules_written_by_user =
if Ordered_set_lang.is_standard modules_written_by_user then
all_modules
else begin
let units =
Ordered_set_lang.eval_with_standard
modules_written_by_user
~standard:(String_map.keys all_modules)
in
List.iter units ~f:(fun unit ->
if not (String_map.mem unit all_modules) then
die "no implementation for module %s in %s"
unit (Path.to_string dir));
let units = String_set.of_list units in
String_map.filter all_modules ~f:(fun unit _ -> String_set.mem unit units)
end
(* +-----------------------------------------------------------------+
| Library stuff |
+-----------------------------------------------------------------+ *)
let lib_archive (lib : Library.t) ~dir ~ext = Path.relative dir (lib.name ^ ext)
let stubs_archive lib ~dir =
Library.stubs_archive lib ~dir ~ext_lib:ctx.ext_lib
let dll (lib : Library.t) ~dir =
Path.relative dir (sprintf "dll%s_stubs%s" lib.name ctx.ext_dll)
let msvc_hack_cclibs cclibs =
let f lib =
if String.is_prefix lib ~prefix:"-l" then
String.sub lib ~pos:2 ~len:(String.length lib - 2) ^ ".lib"
else
lib
in
let cclibs = List.map cclibs ~f in
let f lib =
if String.is_prefix lib ~prefix:"-l" then
String.sub lib ~pos:2 ~len:(String.length lib - 2)
else
lib
in
List.map cclibs ~f
let build_lib (lib : Library.t) ~scope ~flags ~dir ~mode ~modules ~dep_graph =
Option.iter (Context.compiler ctx mode) ~f:(fun compiler ->
let target = lib_archive lib ~dir ~ext:(Mode.compiled_lib_ext mode) in
let dep_graph = Ml_kind.Dict.get dep_graph Impl in
let stubs_flags =
if not (Library.has_stubs lib) then
[]
else
let stubs_name = lib.name ^ "_stubs" in
match mode with
| Byte -> ["-dllib"; "-l" ^ stubs_name; "-cclib"; "-l" ^ stubs_name]
| Native -> ["-cclib"; "-l" ^ stubs_name]
in
let map_cclibs =
(* https://github.com/janestreet/jbuilder/issues/119 *)
if ctx.ccomp_type = "msvc" then
msvc_hack_cclibs
else
fun x -> x
in
let objs (cm, _, _, _) =
if mode = Mode.Byte then
[]
else
List.map ~f:(Path.change_extension ~ext:ctx.ext_obj) cm
in
SC.add_rule sctx
(Build.fanout4
(dep_graph >>>
Build.arr (fun dep_graph ->
Ocamldep.names_to_top_closed_cm_files
~dir
~dep_graph
~modules
~mode
(String_map.keys modules)))
(SC.expand_and_eval_set sctx ~scope ~dir lib.c_library_flags ~standard:[])
(Ocaml_flags.get flags mode)
(SC.expand_and_eval_set sctx ~scope ~dir lib.library_flags ~standard:[])
>>>
Build.dyn_paths (Build.arr objs)
>>>
Build.run ~context:ctx (Ok compiler)
~extra_targets:(
match mode with
| Byte -> []
| Native -> [lib_archive lib ~dir ~ext:ctx.ext_lib])
[ Dyn (fun (_, _, flags, _) -> As flags)
; A "-a"; A "-o"; Target target
; As stubs_flags
; Dyn (fun (_, cclibs, _, _) -> Arg_spec.quote_args "-cclib" (map_cclibs cclibs))
; Dyn (fun (_, _, _, library_flags) -> As library_flags)
; As (match lib.kind with
| Normal -> []
| Ppx_deriver | Ppx_rewriter -> ["-linkall"])
; Dyn (fun (cm_files, _, _, _) -> Deps cm_files)
]))
let build_c_file (lib : Library.t) ~scope ~dir ~requires ~h_files c_name =
let src = Path.relative dir (c_name ^ ".c") in
let dst = Path.relative dir (c_name ^ ctx.ext_obj) in
SC.add_rule sctx
(Build.paths h_files
>>>
Build.fanout
(SC.expand_and_eval_set sctx ~scope ~dir lib.c_flags ~standard:(Context.cc_g ctx))
requires
>>>
Build.run ~context:ctx
(* We have to execute the rule in the library directory as the .o is produced in
the current directory *)
~dir
(Ok ctx.ocamlc)
[ As (Utils.g ())
; Dyn (fun (c_flags, libs) ->
S [ Lib.c_include_flags libs
; Arg_spec.quote_args "-ccopt" c_flags
])
; A "-o"; Target dst
; Dep src
]);
dst
let build_cxx_file (lib : Library.t) ~scope ~dir ~requires ~h_files c_name =
let src = Path.relative dir (c_name ^ ".cpp") in
let dst = Path.relative dir (c_name ^ ctx.ext_obj) in
SC.add_rule sctx
(Build.paths h_files
>>>
Build.fanout
(SC.expand_and_eval_set sctx ~scope ~dir lib.cxx_flags ~standard:(Context.cc_g ctx))
requires
>>>
Build.run ~context:ctx
(* We have to execute the rule in the library directory as the .o is produced in
the current directory *)
~dir
(SC.resolve_program sctx ctx.c_compiler)
[ S [A "-I"; Path ctx.stdlib_dir]
; As (SC.cxx_flags sctx)
; Dyn (fun (cxx_flags, libs) ->
S [ Lib.c_include_flags libs
; As cxx_flags
])
; A "-o"; Target dst
; A "-c"; Dep src
]);
dst
(* Hack for the install file *)
let modules_by_lib : (string, Module.t list) Hashtbl.t = Hashtbl.create 32
(* In 4.02, the compiler reads the cmi for module alias even with [-w -49
-no-alias-deps], so we must sandbox the build of the alias module since the modules
it references are built after. *)
let alias_module_build_sandbox = Scanf.sscanf ctx.version "%u.%u"
(fun a b -> a, b) <= (4, 02)
let library_rules (lib : Library.t) ~dir ~all_modules ~files ~scope =
let dep_kind = if lib.optional then Build.Optional else Required in
let flags = Ocaml_flags.make lib.buildable sctx ~scope ~dir in
let modules =
parse_modules ~dir ~all_modules ~modules_written_by_user:lib.buildable.modules
in
let main_module_name = String.capitalize_ascii lib.name in
let modules =
String_map.map modules ~f:(fun (m : Module.t) ->
if not lib.wrapped || m.name = main_module_name then
{ m with obj_name = Utils.obj_name_of_basename m.impl.name }
else
{ m with obj_name = sprintf "%s__%s" lib.name m.name })
in
let alias_module =
if not lib.wrapped ||
(String_map.cardinal modules = 1 &&
String_map.mem main_module_name modules) then
None
else
let suf =
if String_map.mem main_module_name modules then
"__"
else
""
in
Some
{ Module.name = main_module_name ^ suf
; impl = { name = lib.name ^ suf ^ ".ml-gen" ; syntax = OCaml }
; intf = None
; obj_name = lib.name ^ suf
}
in
(* Add the modules before preprocessing, otherwise the install rules are going to pick
up the pre-processed modules *)
Hashtbl.add modules_by_lib
~key:lib.name
~data:(
let modules =
match alias_module with
| None -> modules
| Some m -> String_map.add modules ~key:m.name ~data:m
in
String_map.values modules);
(* Preprocess before adding the alias module as it doesn't need preprocessing *)
let modules =
SC.PP.pped_modules sctx ~dir ~dep_kind ~modules ~preprocess:lib.buildable.preprocess
~preprocessor_deps:lib.buildable.preprocessor_deps
~lib_name:(Some lib.name)
~scope
in
let modules =
match alias_module with
| None -> modules
| Some m -> String_map.add modules ~key:m.name ~data:m
in
let dep_graph =
Ocamldep.rules sctx ~dir ~item:lib.name ~modules ~alias_module
~lib_interface_module:(if lib.wrapped then
String_map.find main_module_name modules
else
None)
in
Option.iter alias_module ~f:(fun m ->
SC.add_rule sctx
(Build.return
(String_map.values (String_map.remove m.name modules)
|> List.map ~f:(fun (m : Module.t) ->
sprintf "(** @canonical %s.%s *)\n\
module %s = %s\n"
main_module_name m.name
m.name (Module.real_unit_name m))
|> String.concat ~sep:"\n")
>>> Build.write_file_dyn (Path.relative dir m.impl.name)));
let requires, real_requires =
SC.Libs.requires sctx ~dir ~dep_kind ~item:lib.name
~libraries:lib.buildable.libraries
~preprocess:lib.buildable.preprocess
~virtual_deps:lib.virtual_deps
~has_dot_merlin:lib.buildable.gen_dot_merlin
in
SC.Libs.setup_runtime_deps sctx ~dir ~dep_kind ~item:lib.name
~libraries:lib.buildable.libraries
~ppx_runtime_libraries:lib.ppx_runtime_libraries;
SC.Libs.add_select_rules sctx ~dir lib.buildable.libraries;
let dynlink = lib.dynlink in
let js_of_ocaml = lib.buildable.js_of_ocaml in
Module_compilation.build_modules sctx
~js_of_ocaml ~dynlink ~flags ~scope ~dir ~dep_graph ~modules ~requires ~alias_module;
Option.iter alias_module ~f:(fun m ->
let flags = Ocaml_flags.default () in
Module_compilation.build_module sctx m
~js_of_ocaml
~dynlink
~sandbox:alias_module_build_sandbox
~flags:(Ocaml_flags.append_common flags ["-w"; "-49"])
~scope
~dir
~modules:(String_map.singleton m.name m)
~dep_graph:(Ml_kind.Dict.make_both (Build.return (String_map.singleton m.name [])))
~requires:(
let requires =
if String_map.is_empty modules then
(* Just so that we setup lib dependencies for empty libraries *)
requires
else
Build.return []
in
Cm_kind.Dict.of_func (fun ~cm_kind:_ -> requires))
~alias_module:None);
if Library.has_stubs lib then begin
let h_files =
String_set.elements files
|> List.filter_map ~f:(fun fn ->
if String.is_suffix fn ~suffix:".h" then
Some (Path.relative dir fn)
else
None)
in
let o_files =
let requires =
Build.memoize "header files"
(requires >>> SC.Libs.file_deps sctx ~ext:".h")
in
List.map lib.c_names ~f:(build_c_file lib ~scope ~dir ~requires ~h_files) @
List.map lib.cxx_names ~f:(build_cxx_file lib ~scope ~dir ~requires ~h_files)
in
match lib.self_build_stubs_archive with
| Some _ -> ()
| None ->
let ocamlmklib ~sandbox ~custom ~targets =
SC.add_rule sctx ~sandbox
(SC.expand_and_eval_set sctx ~scope ~dir lib.c_library_flags ~standard:[]
>>>
Build.run ~context:ctx
~extra_targets:targets
(Ok ctx.ocamlmklib)
[ As (Utils.g ())
; if custom then A "-custom" else As []
; A "-o"
; Path (Path.relative dir (sprintf "%s_stubs" lib.name))
; Deps o_files
; Dyn (fun cclibs ->
(* https://github.com/janestreet/jbuilder/issues/119 *)
if ctx.ccomp_type = "msvc" then
let cclibs = msvc_hack_cclibs cclibs in
Arg_spec.quote_args "-ldopt" cclibs
else
As cclibs
)
])
in
let static = stubs_archive lib ~dir in
let dynamic = dll lib ~dir in
if lib.modes.native &&
lib.modes.byte &&
lib.dynlink
then begin
(* If we build for both modes and support dynlink, use a single invocation to
build both the static and dynamic libraries *)
ocamlmklib ~sandbox:false ~custom:false ~targets:[static; dynamic]
end else begin
ocamlmklib ~sandbox:false ~custom:true ~targets:[static];
(* We can't tell ocamlmklib to build only the dll, so we sandbox the action to
avoid overriding the static archive *)
ocamlmklib ~sandbox:true ~custom:false ~targets:[dynamic]
end
end;
List.iter Cm_kind.all ~f:(fun cm_kind ->
let files =
String_map.fold modules ~init:[] ~f:(fun ~key:_ ~data:m acc ->
Module.cm_file m ~dir cm_kind :: acc)
in
SC.Libs.setup_file_deps_alias sctx (dir, lib) ~ext:(Cm_kind.ext cm_kind)
files);
SC.Libs.setup_file_deps_group_alias sctx (dir, lib) ~exts:[".cmi"; ".cmx"];
SC.Libs.setup_file_deps_alias sctx (dir, lib) ~ext:".h"
(List.map lib.install_c_headers ~f:(fun header ->
Path.relative dir (header ^ ".h")));
List.iter Mode.all ~f:(fun mode ->
build_lib lib ~scope ~flags ~dir ~mode ~modules ~dep_graph);
(* Build *.cma.js *)
SC.add_rules sctx (
let src = lib_archive lib ~dir ~ext:(Mode.compiled_lib_ext Mode.Byte) in
Js_of_ocaml_rules.build_cm sctx ~scope ~dir ~js_of_ocaml:lib.buildable.js_of_ocaml ~src);
if ctx.natdynlink_supported then
Option.iter ctx.ocamlopt ~f:(fun ocamlopt ->
let src = lib_archive lib ~dir ~ext:(Mode.compiled_lib_ext Native) in
let dst = lib_archive lib ~dir ~ext:".cmxs" in
let build =
Build.dyn_paths (Build.arr (fun () -> [lib_archive lib ~dir ~ext:ctx.ext_lib]))
>>>
Ocaml_flags.get flags Native
>>>
Build.run ~context:ctx
(Ok ocamlopt)
[ Dyn (fun flags -> As flags)
; A "-shared"; A "-linkall"
; A "-I"; Path dir
; A "-o"; Target dst
; Dep src
]
in
let build =
if Library.has_stubs lib then
Build.path (stubs_archive ~dir lib)
>>>
build
else
build
in
SC.add_rule sctx build
);
(* Odoc *)
Odoc.setup_library_rules sctx lib ~dir ~requires ~modules ~dep_graph;
let flags =
match alias_module with
| None -> Ocaml_flags.common flags
| Some m -> Ocaml_flags.prepend_common ["-open"; m.name] flags |> Ocaml_flags.common
in
{ Merlin.
requires = real_requires
; flags
; preprocess = Buildable.single_preprocess lib.buildable
; libname = Some lib.name
; source_dirs = Path.Set.empty
}
(* +-----------------------------------------------------------------+
| Executables stuff |
+-----------------------------------------------------------------+ *)
let build_exe ~js_of_ocaml ~flags ~scope ~dir ~requires ~name ~mode ~modules ~dep_graph
~link_flags ~force_custom_bytecode =
let exe_ext = Mode.exe_ext mode in
let mode, link_custom, compiler =
match force_custom_bytecode, Context.compiler ctx mode with
| false, Some compiler -> (mode, [], compiler)
| _ -> (Byte, ["-custom"], ctx.ocamlc)
in
let dep_graph = Ml_kind.Dict.get dep_graph Impl in
let exe = Path.relative dir (name ^ exe_ext) in
let libs_and_cm =
Build.fanout
(requires
>>> Build.dyn_paths (Build.arr (Lib.archive_files ~mode ~ext_lib:ctx.ext_lib)))
(dep_graph
>>> Build.arr (fun dep_graph ->
Ocamldep.names_to_top_closed_cm_files
~dir
~dep_graph
~modules
~mode
[String.capitalize_ascii name]))
in
let objs (libs, cm) =
if mode = Mode.Byte then
[]
else
let libs =
let f = function
| Lib.Internal (dir, lib) -> Some (Path.relative dir (lib.name ^ ctx.ext_lib))
| External _ -> None
in
List.filter_map ~f libs
in
libs @ List.map ~f:(Path.change_extension ~ext:ctx.ext_obj) cm
in
SC.add_rule sctx
((libs_and_cm >>> Build.dyn_paths (Build.arr objs))
&&&
Build.fanout
(Ocaml_flags.get flags mode)
(SC.expand_and_eval_set sctx ~scope ~dir link_flags ~standard:[])
>>>
Build.run ~context:ctx
(Ok compiler)
[ Dyn (fun (_, (flags,_)) -> As flags)
; A "-o"; Target exe
; Dyn (fun (_, (_, link_flags)) -> As (link_custom @ link_flags))
; Dyn (fun ((libs, _), _) -> Lib.link_flags libs ~mode)
; Dyn (fun ((_, cm_files), _) -> Deps cm_files)
]);
if mode = Mode.Byte then
let rules = Js_of_ocaml_rules.build_exe sctx ~dir ~js_of_ocaml ~src:exe in
let libs_and_cm_and_flags =
libs_and_cm
&&&
SC.expand_and_eval_set sctx ~scope ~dir js_of_ocaml.flags ~standard:(Js_of_ocaml_rules.standard ())
in
SC.add_rules sctx (List.map rules ~f:(fun r -> libs_and_cm_and_flags >>> r))
let executables_rules (exes : Executables.t) ~dir ~all_modules ~scope =
let dep_kind = Build.Required in
let flags = Ocaml_flags.make exes.buildable sctx ~scope ~dir in
let modules =
parse_modules ~dir ~all_modules ~modules_written_by_user:exes.buildable.modules
in
let modules =
String_map.map modules ~f:(fun (m : Module.t) ->
{ m with obj_name = Utils.obj_name_of_basename m.impl.name })
in
List.iter exes.names ~f:(fun name ->
if not (String_map.mem (String.capitalize_ascii name) modules) then
die "executable %s in %s doesn't have a corresponding .ml file"
name (Path.to_string dir));
let modules =
SC.PP.pped_modules sctx ~dir ~dep_kind ~modules
~preprocess:exes.buildable.preprocess
~preprocessor_deps:exes.buildable.preprocessor_deps
~lib_name:None
~scope
in
let item = List.hd exes.names in
let dep_graph =
Ocamldep.rules sctx ~dir ~item ~modules ~alias_module:None
~lib_interface_module:None
in
let requires, real_requires =
SC.Libs.requires sctx ~dir ~dep_kind ~item
~libraries:exes.buildable.libraries
~preprocess:exes.buildable.preprocess
~virtual_deps:[]
~has_dot_merlin:exes.buildable.gen_dot_merlin
in
SC.Libs.add_select_rules sctx ~dir exes.buildable.libraries;
(* CR-someday jdimino: this should probably say [~dynlink:false] *)
Module_compilation.build_modules sctx
~js_of_ocaml:exes.buildable.js_of_ocaml
~dynlink:true ~flags ~scope ~dir ~dep_graph ~modules
~requires ~alias_module:None;
List.iter exes.names ~f:(fun name ->
List.iter Mode.all ~f:(fun mode ->
build_exe ~js_of_ocaml:exes.buildable.js_of_ocaml ~flags ~scope ~dir ~requires ~name
~mode ~modules ~dep_graph ~link_flags:exes.link_flags
~force_custom_bytecode:(mode = Native && not exes.modes.native)));
{ Merlin.
requires = real_requires
; flags = Ocaml_flags.common flags
; preprocess = Buildable.single_preprocess exes.buildable
; libname = None
; source_dirs = Path.Set.empty
}
(* +-----------------------------------------------------------------+
| User rules |
+-----------------------------------------------------------------+ *)
let interpret_locks ~dir ~scope locks =
List.map locks ~f:(fun s ->
Path.relative dir (SC.expand_vars sctx ~dir ~scope s))
let user_rule (rule : Rule.t) ~dir ~scope =
let targets : SC.Action.targets =
match rule.targets with
| Infer -> Infer
| Static fns -> Static (List.map fns ~f:(Path.relative dir))
in
SC.add_rule sctx ~fallback:rule.fallback ~loc:rule.loc
~locks:(interpret_locks ~dir ~scope rule.locks)
(SC.Deps.interpret sctx ~scope ~dir rule.deps
>>>
SC.Action.run
sctx
rule.action
~dir
~dep_kind:Required
~targets
~scope)
let alias_rules (alias_conf : Alias_conf.t) ~dir ~scope =
let digest =
let deps =
Sexp.To_sexp.list Dep_conf.sexp_of_t alias_conf.deps in
let action =
match alias_conf.action with
| None -> Sexp.Atom "none"
| Some a -> List [Atom "some" ; Action.Unexpanded.sexp_of_t a]
in
Sexp.List [deps ; action]
|> Sexp.to_string
|> Digest.string
in
let alias = Alias.make alias_conf.name ~dir in
let digest_path = Alias.file_with_digest_suffix alias ~digest in
Alias.add_deps (SC.aliases sctx) alias [digest_path];
let deps = SC.Deps.interpret sctx ~scope ~dir alias_conf.deps in
SC.add_rule sctx
~locks:(interpret_locks ~dir ~scope alias_conf.locks)
(match alias_conf.action with
| None ->
deps
>>>
Build.create_file digest_path
| Some action ->
deps
>>>
Build.progn
[ SC.Action.run
sctx
action
~dir
~dep_kind:Required
~targets:(Static [])
~scope
; Build.create_file digest_path
])
let copy_files_rules (def: Copy_files.t) ~src_dir ~dir ~scope =
let loc = String_with_vars.loc def.glob in
let glob_in_src =
let src_glob = SC.expand_vars sctx ~dir def.glob ~scope in
Path.relative src_dir src_glob ~error_loc:loc
in
(* The following condition is required for merlin to work.
Additionally, the order in which the rules are evaluated only
ensures that [sources_and_targets_known_so_far] returns the
right answer for sub-directories only. *)
if not (Path.is_descendant glob_in_src ~of_:src_dir) then
Loc.fail loc "%s is not a sub-directory of %s"
(Path.to_string_maybe_quoted glob_in_src) (Path.to_string_maybe_quoted src_dir);
let glob = Path.basename glob_in_src in
let src_in_src = Path.parent glob_in_src in
let re =
match Glob_lexer.parse_string glob with
| Ok re ->
Re.compile re
| Error (_pos, msg) ->
Loc.fail (String_with_vars.loc def.glob) "invalid glob: %s" msg
in
(* add rules *)
let files = SC.sources_and_targets_known_so_far sctx ~src_path:src_in_src in
let src_in_build = Path.append ctx.build_dir src_in_src in
String_set.iter files ~f:(fun basename ->
let matches = Re.execp re basename in
if matches then
let file_src = Path.relative src_in_build basename in
let file_dst = Path.relative dir basename in
SC.add_rule sctx
((if def.add_line_directive
then Build.copy_and_add_line_directive
else Build.copy)
~src:file_src
~dst:file_dst)
);
{ Merlin.requires = Build.return []
; flags = Build.return []
; preprocess = Jbuild.Preprocess.No_preprocessing
; libname = None
; source_dirs = Path.Set.singleton src_in_src
}
(* +-----------------------------------------------------------------+
| Modules listing |
+-----------------------------------------------------------------+ *)
let ml_of_mli : _ format =
{|(with-stdout-to %s
(progn
(echo "[@@@warning \"-a\"]\nmodule rec HACK : sig\n")
(cat %s)
(echo "\nend = HACK\ninclude HACK\n")))|}
let re_of_rei : _ format =
{|(with-stdout-to %s
(progn
(echo "[@@@warning \"-a\"];\nmodule type HACK = {\n")
(cat %s)
(echo "\n};\nmodule rec HACK : HACK = HACK;\ninclude HACK;\n")))|}
let no_impl_warning : _ format =
{|@{<warning>Warning@}: Module %s in %s doesn't have a corresponding .%s file.
Modules without an implementation are not recommended, see this discussion:
https://github.com/janestreet/jbuilder/issues/9
In the meantime I'm implicitely adding this rule:
(rule %s)
Add it to your jbuild file to remove this warning.
|}
let guess_modules ~dir ~files =
let impl_files, intf_files =
String_set.elements files
|> List.filter_map ~f:(fun fn ->
(* we aren't using Filename.extension because we want to handle
filenames such as foo.cppo.ml *)
match String.lsplit2 fn ~on:'.' with
| Some (_, "ml") -> Some (Inl { Module.File.syntax=OCaml ; name=fn })
| Some (_, "re") -> Some (Inl { Module.File.syntax=Reason ; name=fn })
| Some (_, "mli") -> Some (Inr { Module.File.syntax=OCaml ; name=fn })
| Some (_, "rei") -> Some (Inr { Module.File.syntax=Reason ; name=fn })
| _ -> None)
|> List.partition_map ~f:(fun x -> x) in
let parse_one_set files =
List.map files ~f:(fun (f : Module.File.t) ->
(String.capitalize_ascii (Filename.chop_extension f.name), f))
|> String_map.of_alist
|> function
| Ok x -> x
| Error (name, f1, f2) ->
die "too many files for module %s in %s: %s and %s"
name (Path.to_string dir) f1.name f2.name
in
let impls = parse_one_set impl_files in
let intfs = parse_one_set intf_files in
let setup_intf_only name (intf : Module.File.t) =
let impl_fname = String.sub intf.name ~pos:0 ~len:(String.length intf.name - 1) in
let action_str =
sprintf
(match intf.syntax with
| OCaml -> ml_of_mli
| Reason -> re_of_rei)
impl_fname intf.name
in
Format.eprintf no_impl_warning
name (Path.to_string dir)
(match intf.syntax with
| OCaml -> "ml"
| Reason -> "re")
action_str;
let dir = Path.append ctx.build_dir dir in
let action =
Usexp.parse_string action_str
~fname:"<internal action for mli to ml>"
~mode:Single
|> Action.Unexpanded.t
in
SC.add_rule sctx
(Build.return []
>>>
SC.Action.run sctx action
~dir
~dep_kind:Required
~targets:Infer
~scope:Scope.empty);
{ intf with name = impl_fname } in
String_map.merge impls intfs ~f:(fun name impl intf ->
let impl =
match impl with
| None -> setup_intf_only name (Option.value_exn intf)
| Some i -> i in
Some
{ Module.name
; impl
; intf
; obj_name = "" }
)
(* +-----------------------------------------------------------------+
| Stanza |
+-----------------------------------------------------------------+ *)
let rules { SC.Dir_with_jbuild. src_dir; ctx_dir; stanzas; scope } =
(* Interpret user rules and other simple stanzas first in order to populate the known
target table, which is needed for guessing the list of modules. *)
let merlins =
List.filter_map stanzas ~f:(fun stanza ->
let dir = ctx_dir in
match (stanza : Stanza.t) with
| Rule rule -> user_rule rule ~dir ~scope; None
| Alias alias -> alias_rules alias ~dir ~scope; None
| Copy_files def ->
Some (copy_files_rules def ~src_dir ~dir ~scope)
| Library _ | Executables _ | Provides _ | Install _ -> None)
in
let files = lazy (
let files = SC.sources_and_targets_known_so_far sctx ~src_path:src_dir in
(* Manually add files generated by the (select ...) dependencies since we haven't
interpreted libraries and executables yet. *)
List.fold_left stanzas ~init:files ~f:(fun acc stanza ->
match (stanza : Stanza.t) with
| Library { buildable; _ } | Executables { buildable; _ } ->
List.fold_left buildable.libraries ~init:acc ~f:(fun acc dep ->
match (dep : Jbuild.Lib_dep.t) with
| Direct _ -> acc
| Select s -> String_set.add s.result_fn acc)
| _ -> acc)
) in
let all_modules = lazy (
guess_modules ~dir:src_dir
~files:(Lazy.force files))
in
List.fold_left stanzas ~init:merlins ~f:(fun merlins stanza ->
let dir = ctx_dir in
match (stanza : Stanza.t) with
| Library lib ->
library_rules lib ~dir ~all_modules:(Lazy.force all_modules)
~files:(Lazy.force files) ~scope
:: merlins
| Executables exes ->
executables_rules exes ~dir ~all_modules:(Lazy.force all_modules) ~scope
:: merlins
| _ -> merlins)
|> Merlin.merge_all
|> Option.map ~f:(fun (m : Merlin.t) ->
{ m with source_dirs =
Path.Set.add (Path.relative src_dir ".") m.source_dirs
})
|> Option.iter ~f:(Merlin.add_rules sctx ~dir:ctx_dir);
Option.iter (Utop.exe_stanzas stanzas) ~f:(fun (exe, all_modules) ->
let dir = Utop.utop_exe_dir ~dir:ctx_dir in
let merlin = executables_rules exe ~dir ~all_modules ~scope in
Utop.add_module_rules sctx ~dir merlin.requires;
)
let () =
(* Sort the list of stanzas by directory so that we traverse
subdirectories first.
This is required for correctly interpreting [copy_files]. *)
let subtree_smaller x y =
Path.compare y.SC.Dir_with_jbuild.src_dir x.SC.Dir_with_jbuild.src_dir in
let stanzas = List.sort ~cmp:subtree_smaller (SC.stanzas sctx) in
List.iter stanzas ~f:rules
let () =
SC.add_rules sctx (Js_of_ocaml_rules.setup_separate_compilation_rules sctx)
let () = Odoc.setup_css_rule sctx
let () = Odoc.setup_toplevel_index_rule sctx
(* +-----------------------------------------------------------------+
| META |
+-----------------------------------------------------------------+ *)
(* The rules for META files must come after the interpretation of the jbuild stanzas
since a user rule might generate a META.<package> file *)
(* META files that must be installed. Either because there is an explicit or user
generated one, or because *)
let packages_with_explicit_or_user_generated_meta =
String_map.values (SC.packages sctx)
|> List.filter_map ~f:(fun (pkg : Package.t) ->
let path = Path.append ctx.build_dir pkg.path in
let meta_fn = "META." ^ pkg.name in
let meta_templ_fn = meta_fn ^ ".template" in
let files =
SC.sources_and_targets_known_so_far sctx ~src_path:pkg.path
in
let has_meta, has_meta_tmpl =
(String_set.mem meta_fn files,
String_set.mem meta_templ_fn files)
in
let meta_fn =
if has_meta then
meta_fn ^ ".from-jbuilder"
else
meta_fn
in
let meta_path = Path.relative path meta_fn in
let version =
let get =
match pkg.version_from_opam_file with
| Some s -> Build.return (Some s)
| None ->
let rec loop = function
| [] -> Build.return None
| candidate :: rest ->
let p = Path.relative path candidate in
Build.if_file_exists p
~then_:(Build.lines_of p
>>^ function
| ver :: _ -> Some ver
| _ -> Some "")
~else_:(loop rest)
in
loop
[ pkg.name ^ ".version"
; "version"
; "VERSION"
]
in
Super_context.Pkg_version.set sctx pkg get
in
let template =
if has_meta_tmpl then
let meta_templ_path = Path.relative path meta_templ_fn in
Build.lines_of meta_templ_path
else
Build.return ["# JBUILDER_GEN"]
in
let meta =
Gen_meta.gen ~package:pkg.name
~version
~stanzas:(SC.stanzas_to_consider_for_install sctx)
~lib_deps:(fun ~dir jbuild ->
match jbuild with
| Library lib ->
Build.arr ignore
>>>
SC.Libs.load_requires sctx ~dir ~item:lib.name
>>^ List.map ~f:Lib.best_name
| Executables exes ->
let item = List.hd exes.names in
Build.arr ignore
>>>
SC.Libs.load_requires sctx ~dir ~item
>>^ List.map ~f:Lib.best_name
| _ -> Build.arr (fun _ -> []))
~ppx_runtime_deps:(fun ~dir jbuild ->
match jbuild with
| Library lib ->
Build.arr ignore
>>>
SC.Libs.load_runtime_deps sctx ~dir ~item:lib.name
>>^ List.map ~f:Lib.best_name
| _ -> Build.arr (fun _ -> []))
in
SC.add_rule sctx
(Build.fanout meta template
>>^ (fun ((meta : Meta.t), template) ->
let buf = Buffer.create 1024 in
let ppf = Format.formatter_of_buffer buf in
Format.pp_open_vbox ppf 0;
List.iter template ~f:(fun s ->
if String.is_prefix s ~prefix:"#" then
match
String.extract_blank_separated_words
(String.sub s ~pos:1 ~len:(String.length s - 1))
with
| ["JBUILDER_GEN"] -> Format.fprintf ppf "%a@," Meta.pp meta.entries
| _ -> Format.fprintf ppf "%s@," s
else
Format.fprintf ppf "%s@," s);
Format.pp_close_box ppf ();
Format.pp_print_flush ppf ();
Buffer.contents buf)
>>>
Build.write_file_dyn meta_path);
if has_meta || has_meta_tmpl then
Some pkg.name
else
None)
|> String_set.of_list
(* +-----------------------------------------------------------------+
| Installation |
+-----------------------------------------------------------------+ *)
let lib_install_files ~dir ~sub_dir (lib : Library.t) =
let make_entry section fn =
Install.Entry.make section fn
?dst:(Option.map sub_dir ~f:(fun d -> sprintf "%s/%s" d (Path.basename fn)))
in
let { Mode.Dict. byte; native } = lib.modes in
let if_ cond l = if cond then l else [] in
let files =
let modules =
Hashtbl.find_exn modules_by_lib lib.name
~string_of_key:(sprintf "%S")
~table_desc:(fun _ ->
sprintf "<module table for context %s>"
(Path.to_string ctx.build_dir))
in
List.concat
[ List.concat_map modules ~f:(fun m ->
List.concat
[ [ Module.cm_file m ~dir Cmi ]
; if_ native [ Module.cm_file m ~dir Cmx ]
; List.filter_map Ml_kind.all ~f:(Module.cmt_file m ~dir)
; [ match Module.file m ~dir Intf with
| Some fn -> fn
| None -> Path.relative dir m.impl.name ]
])
; if_ byte [ lib_archive ~dir lib ~ext:".cma" ]
; if_ (Library.has_stubs lib) [ stubs_archive ~dir lib ]
; if_ native
(match ctx.ocamlopt with
| None -> []
| Some _ ->
let files =
[ lib_archive ~dir lib ~ext:".cmxa"
; lib_archive ~dir lib ~ext:ctx.ext_lib
]
in
if ctx.natdynlink_supported && lib.dynlink then
files @ [ lib_archive ~dir lib ~ext:".cmxs" ]
else
files
)
; List.map lib.buildable.js_of_ocaml.javascript_files ~f:(Path.relative dir)
; List.map lib.install_c_headers ~f:(fun fn ->
Path.relative dir (fn ^ ".h"))
]
in
let dlls = if_ (byte && Library.has_stubs lib && lib.dynlink) [dll ~dir lib] in
let execs =
match lib.kind with
| Normal | Ppx_deriver -> []
| Ppx_rewriter ->
let pps = [Pp.of_string lib.name] in
let pps =
(* This is a temporary hack until we get a standard driver *)
let deps = List.concat_map lib.buildable.libraries ~f:Lib_dep.to_lib_names in
if List.exists deps ~f:(function
| "ppx_driver" | "ppx_type_conv" -> true
| _ -> false) then
pps @ [Pp.of_string "ppx_driver.runner"]
else
pps
in
let ppx_exe =
SC.PP.get_ppx_driver sctx pps
~dir ~dep_kind:(if lib.optional then Build.Optional else Required)
in
[ppx_exe]
in
List.concat
[ List.map files ~f:(make_entry Lib )
; List.map execs ~f:(make_entry Libexec)
; List.map dlls ~f:(Install.Entry.make Stublibs)
]
let is_odig_doc_file fn =
List.exists [ "README"; "LICENSE"; "CHANGE"; "HISTORY"]
~f:(fun prefix -> String.is_prefix fn ~prefix)
let local_install_rules (entries : Install.Entry.t list) ~package =
let install_dir = Config.local_install_dir ~context:ctx.name in
List.map entries ~f:(fun entry ->
let dst =
Path.append install_dir (Install.Entry.relative_installed_path entry ~package)
in
SC.add_rule sctx (Build.symlink ~src:entry.src ~dst);
Install.Entry.set_src entry dst)
let install_file package_path package entries =
let entries =
let files = SC.sources_and_targets_known_so_far sctx ~src_path:Path.root in
String_set.fold files ~init:entries ~f:(fun fn acc ->
if is_odig_doc_file fn then
Install.Entry.make Doc (Path.relative ctx.build_dir fn) :: acc
else
acc)
in
let entries =
let opam = Path.relative package_path (package ^ ".opam") in
Install.Entry.make Lib opam ~dst:"opam" :: entries
in
let entries =
(* Install a META file if the user wrote one or setup a rule to generate one, or if
we have at least another file to install in the lib/ directory *)
let meta_fn = "META." ^ package in
if String_set.mem package packages_with_explicit_or_user_generated_meta ||
List.exists entries ~f:(fun (e : Install.Entry.t) -> e.section = Lib) then
let meta = Path.append ctx.build_dir (Path.relative package_path meta_fn) in
Install.Entry.make Lib meta ~dst:"META" :: entries
else
entries
in
let fn =
Path.relative (Path.append ctx.build_dir package_path) (package ^ ".install")
in
let entries = local_install_rules entries ~package in
SC.add_rule sctx
(Build.path_set (Install.files entries)
>>^ (fun () ->
Install.gen_install_file entries)
>>>
Build.write_file_dyn fn)
let () =
let entries_per_package =
List.concat_map (SC.stanzas_to_consider_for_install sctx)
~f:(fun (dir, stanza) ->
match stanza with
| Library ({ public = Some { package; sub_dir; _ }; _ } as lib) ->
List.map (lib_install_files ~dir ~sub_dir lib) ~f:(fun x ->
package.name, x)
| Install { section; files; package}->
List.map files ~f:(fun { Install_conf. src; dst } ->
(package.name, Install.Entry.make section (Path.relative dir src) ?dst))
| _ -> [])
|> String_map.of_alist_multi
in
String_map.iter (SC.packages sctx) ~f:(fun ~key:_ ~data:(pkg : Package.t) ->
let stanzas = String_map.find_default pkg.name entries_per_package ~default:[] in
install_file pkg.path pkg.name stanzas)
let () =
let is_default = Path.basename ctx.build_dir = "default" in
String_map.iter (SC.packages sctx)
~f:(fun ~key:pkg ~data:{ Package.path = src_path; _ } ->
let install_fn = pkg ^ ".install" in
let ctx_path = Path.append ctx.build_dir src_path in
let ctx_install_alias = Alias.install ~dir:ctx_path in
let ctx_install_file = Path.relative ctx_path install_fn in
Alias.add_deps (SC.aliases sctx) ctx_install_alias [ctx_install_file];
if is_default then begin
let src_install_alias = Alias.install ~dir:src_path in
let src_install_file = Path.relative src_path install_fn in
SC.add_rule sctx (Build.copy ~src:ctx_install_file ~dst:src_install_file);
Alias.add_deps (SC.aliases sctx) src_install_alias [src_install_file]
end)
end
let gen ~contexts ?(filter_out_optional_stanzas_with_missing_deps=true)
?only_packages ?(unlink_aliases=[]) conf =
let open Future in
let { Jbuild_load. file_tree; jbuilds; packages; scopes } = conf in
let aliases = Alias.Store.create () in
let packages =
match only_packages with
| None -> packages
| Some pkgs ->
String_map.filter packages ~f:(fun _ { Package.name; _ } ->
String_set.mem name pkgs)
in
List.map contexts ~f:(fun context ->
Jbuild_load.Jbuilds.eval ~context jbuilds >>| fun stanzas ->
let stanzas =
match only_packages with
| None -> stanzas
| Some pkgs ->
List.map stanzas ~f:(fun (dir, pkgs_ctx, stanzas) ->
(dir,
pkgs_ctx,
List.filter stanzas ~f:(fun stanza ->
match (stanza : Stanza.t) with
| Library { public = Some { package; _ }; _ }
| Alias { package = Some package ; _ }
| Install { package; _ } ->
String_set.mem package.name pkgs
| _ -> true)))
in
let sctx =
Super_context.create
~context
~aliases
~scopes
~file_tree
~packages
~filter_out_optional_stanzas_with_missing_deps
~stanzas
in
let module M = Gen(struct let sctx = sctx end) in
(Super_context.rules sctx, (context.name, stanzas)))
|> Future.all
>>| fun l ->
let rules, context_names_and_stanzas = List.split l in
Alias.Store.unlink aliases unlink_aliases;
(Alias.rules aliases @ List.concat rules,
String_map.of_alist_exn context_names_and_stanzas)