dune/src/super_context.ml

1117 lines
37 KiB
OCaml

open Import
open Jbuild
module A = Action
module Pset = Path.Set
module Alias = Build_system.Alias
module Dir_with_jbuild = struct
type t =
{ src_dir : Path.t
; ctx_dir : Path.t
; stanzas : Stanzas.t
; scope : Scope.t
}
end
type t =
{ context : Context.t
; build_system : Build_system.t
; scopes : Scope.DB.t
; public_libs : Lib.DB.t
; installed_libs : Lib.DB.t
; stanzas : Dir_with_jbuild.t list
; packages : Package.t String_map.t
; file_tree : File_tree.t
; artifacts : Artifacts.t
; stanzas_to_consider_for_install : (Path.t * Scope.t * Stanza.t) list
; cxx_flags : string list
; vars : Action.Var_expansion.t String_map.t
; ppx_dir : Path.t
; chdir : (Action.t, Action.t) Build.t
; host : t option
}
let context t = t.context
let stanzas t = t.stanzas
let packages t = t.packages
let artifacts t = t.artifacts
let file_tree t = t.file_tree
let stanzas_to_consider_for_install t = t.stanzas_to_consider_for_install
let cxx_flags t = t.cxx_flags
let host_sctx t = Option.value t.host ~default:t
let public_libs t = t.public_libs
let installed_libs t = t.installed_libs
let find_scope_by_dir t dir = Scope.DB.find_by_dir t.scopes dir
let find_scope_by_name t name = Scope.DB.find_by_name t.scopes name
let expand_var_no_root t var = String_map.find var t.vars
let expand_vars t ~scope ~dir s =
String_with_vars.expand s ~f:(fun _loc -> function
| "ROOT" -> Some (Path.reach ~from:dir t.context.build_dir)
| "SCOPE_ROOT" ->
Some (Path.reach ~from:dir (Scope.root scope))
| var ->
expand_var_no_root t var
|> Option.map ~f:(fun e -> Action.Var_expansion.to_string dir e))
let resolve_program t ?hint bin =
Artifacts.binary ?hint t.artifacts bin
let create
~(context:Context.t)
?host
~scopes
~file_tree
~packages
~stanzas
~filter_out_optional_stanzas_with_missing_deps
~build_system
=
let installed_libs = Lib.DB.create_from_findlib context.findlib in
let internal_libs =
List.concat_map stanzas ~f:(fun (dir, _, stanzas) ->
let ctx_dir = Path.append context.build_dir dir in
List.filter_map stanzas ~f:(fun stanza ->
match (stanza : Stanza.t) with
| Library lib -> Some (ctx_dir, lib)
| _ -> None))
in
let scopes, public_libs =
let scopes =
List.map scopes ~f:(fun (scope : Scope_info.t) ->
{ scope with root = Path.append context.build_dir scope.root })
in
Scope.DB.create
~scopes
~context:context.name
~installed_libs
internal_libs
in
let stanzas =
List.map stanzas
~f:(fun (dir, scope, stanzas) ->
let ctx_dir = Path.append context.build_dir dir in
{ Dir_with_jbuild.
src_dir = dir
; ctx_dir
; stanzas
; scope = Scope.DB.find_by_name scopes scope.Scope_info.name
})
in
let stanzas_to_consider_for_install =
if filter_out_optional_stanzas_with_missing_deps then
List.concat_map stanzas ~f:(fun { ctx_dir; stanzas; scope; _ } ->
List.filter_map stanzas ~f:(fun stanza ->
let keep =
match (stanza : Stanza.t) with
| Library lib -> Lib.DB.available (Scope.libs scope) lib.name
| Install _ -> true
| _ -> false
in
Option.some_if keep (ctx_dir, scope, stanza)))
else
List.concat_map stanzas ~f:(fun { ctx_dir; stanzas; scope; _ } ->
List.map stanzas ~f:(fun s -> (ctx_dir, scope, s)))
in
let artifacts =
Artifacts.create context ~public_libs stanzas
~f:(fun (d : Dir_with_jbuild.t) -> d.stanzas)
in
let cxx_flags =
String.extract_blank_separated_words context.ocamlc_cflags
|> List.filter ~f:(fun s -> not (String.is_prefix s ~prefix:"-std="))
in
let vars =
let ocamlopt =
match context.ocamlopt with
| None -> Path.relative context.ocaml_bin "ocamlopt"
| Some p -> p
in
let open Action.Var_expansion in
let make =
match Bin.make with
| None -> Strings (["make"], Split)
| Some p -> Paths ([p], Split)
in
let cflags = String.extract_blank_separated_words context.ocamlc_cflags in
[ "-verbose" , Strings ([] (*"-verbose";*), Concat)
; "CPP" , Strings (context.c_compiler :: cflags @ ["-E"], Split)
; "PA_CPP" , Strings (context.c_compiler :: cflags
@ ["-undef"; "-traditional"; "-x"; "c"; "-E"],
Split)
; "CC" , Strings (context.c_compiler :: cflags, Split)
; "CXX" , Strings (context.c_compiler :: cxx_flags, Split)
; "ocaml_bin" , Paths ([context.ocaml_bin], Split)
; "OCAML" , Paths ([context.ocaml], Split)
; "OCAMLC" , Paths ([context.ocamlc], Split)
; "OCAMLOPT" , Paths ([ocamlopt], Split)
; "ocaml_version" , Strings ([context.version_string], Concat)
; "ocaml_where" , Paths ([context.stdlib_dir], Concat)
; "ARCH_SIXTYFOUR" , Strings ([string_of_bool context.arch_sixtyfour],
Concat)
; "MAKE" , make
; "null" , Paths ([Config.dev_null], Concat)
]
|> String_map.of_alist
|> function
| Ok x -> x
| Error _ -> assert false
in
{ context
; host
; build_system
; scopes
; public_libs
; installed_libs
; stanzas
; packages
; file_tree
; stanzas_to_consider_for_install
; artifacts
; cxx_flags
; vars
; ppx_dir = Path.relative context.build_dir ".ppx"
; chdir = Build.arr (fun (action : Action.t) ->
match action with
| Chdir _ -> action
| _ -> Chdir (context.build_dir, action))
}
let add_rule t ?sandbox ?mode ?locks ?loc build =
let build = Build.O.(>>>) build t.chdir in
Build_system.add_rule t.build_system
(Build_interpret.Rule.make ?sandbox ?mode ?locks ?loc
~context:t.context build)
let add_rule_get_targets t ?sandbox ?mode ?locks ?loc build =
let build = Build.O.(>>>) build t.chdir in
let rule =
Build_interpret.Rule.make ?sandbox ?mode ?locks ?loc
~context:t.context build
in
Build_system.add_rule t.build_system rule;
List.map rule.targets ~f:Build_interpret.Target.path
let add_rules t ?sandbox builds =
List.iter builds ~f:(add_rule t ?sandbox)
let add_alias_deps t alias deps =
Alias.add_deps t.build_system alias deps
let add_alias_action t alias ?locks ~stamp action =
Alias.add_action t.build_system alias ?locks ~stamp action
let eval_glob t ~dir re = Build_system.eval_glob t.build_system ~dir re
let load_dir t ~dir = Build_system.load_dir t.build_system ~dir
let on_load_dir t ~dir ~f = Build_system.on_load_dir t.build_system ~dir ~f
let source_files t ~src_path =
match File_tree.find_dir t.file_tree src_path with
| None -> String_set.empty
| Some dir -> File_tree.Dir.files dir
module Libs = struct
open Build.O
let requires_to_build requires ~required_by =
match requires with
| Ok x -> Build.return x
| Error e ->
Build.fail
{ fail = fun () ->
raise (Lib.Error (With_required_by.append e required_by))
}
let requires_generic
t
~loc
~dir
~requires
~libraries
~dep_kind
~has_dot_merlin
=
let requires =
requires_to_build requires ~required_by:[Loc loc]
in
let requires =
Build.record_lib_deps ~kind:dep_kind libraries
>>> requires
in
let requires_with_merlin =
if t.context.merlin && has_dot_merlin then
Build.path (Path.relative dir ".merlin-exists")
>>>
requires
else
requires
in
(requires_with_merlin, requires)
let add_select_rules t ~dir resolved_selects =
List.iter resolved_selects ~f:(fun rs ->
let { Lib.Compile.Resolved_select.dst_fn; src_fn } = rs in
let dst = Path.relative dir dst_fn in
add_rule t
(match src_fn with
| Ok src_fn ->
let src = Path.relative dir src_fn in
Build.copy_and_add_line_directive ~src ~dst
| Error e ->
Build.fail ~targets:[dst]
{ fail = fun () ->
raise (Lib.Error { data = No_solution_found_for_select e
; required_by = []
})
}))
let requires_for_library t ~dir ~scope ~dep_kind (conf : Jbuild.Library.t) =
match Lib.DB.find (Scope.libs scope) conf.name with
| Error Not_found -> assert false
| Error (Hidden _ as reason) ->
let build =
Build.fail { fail = fun () ->
Lib.not_available ~loc:conf.buildable.loc reason "Library %S"
conf.name }
in
(build, build)
| Ok lib ->
add_select_rules t ~dir (Lib.Compile.resolved_selects lib);
let libraries =
List.fold_left conf.virtual_deps ~init:conf.buildable.libraries
~f:(fun acc s -> Lib_dep.Direct s :: acc)
in
requires_generic t ~dir ~loc:conf.buildable.loc
~requires:(Lib.Compile.requires lib)
~libraries
~dep_kind
~has_dot_merlin:true
let requires t ~loc ~dir ~scope ~dep_kind ~libraries
~preprocess ~has_dot_merlin =
let requires, resolved_selects =
Lib.DB.resolve_user_written_deps (Scope.libs scope)
libraries
~pps:(Jbuild.Preprocess_map.pps preprocess)
in
add_select_rules t ~dir resolved_selects;
requires_generic t ~dir ~loc
~requires
~libraries
~dep_kind
~has_dot_merlin
let lib_files_alias ~dir ~name ~ext =
Alias.make (sprintf "lib-%s%s-all" name ext) ~dir
let setup_file_deps_alias t ~dir ~ext lib files =
add_alias_deps t
(lib_files_alias ~dir ~name:(Library.best_name lib) ~ext) files
let setup_file_deps_group_alias t ~dir ~exts lib =
setup_file_deps_alias t lib ~dir
~ext:(String.concat exts ~sep:"-and-")
(List.map exts ~f:(fun ext ->
Alias.stamp_file
(lib_files_alias ~dir ~name:(Library.best_name lib) ~ext)))
let file_deps t ~ext =
Build.dyn_paths (Build.arr (fun libs ->
List.fold_left libs ~init:[] ~f:(fun acc (lib : Lib.t) ->
let x =
if Lib.is_local lib then
Alias.stamp_file
(lib_files_alias ~dir:(Lib.src_dir lib) ~name:(Lib.name lib) ~ext)
else
Build_system.stamp_file_for_files_of t.build_system
~dir:(Lib.obj_dir lib) ~ext
in
x :: acc)))
end
module Doc = struct
let root t = Path.relative t.context.Context.build_dir "_doc"
type origin =
| Public of string
| Private of string * Scope_info.Name.t
let dir_internal t origin =
let name =
match origin with
| Public n -> n
| Private (n, s) -> sprintf "%s@%s" n (Scope_info.Name.to_string s)
in
Path.relative (root t) name
let dir t (lib : Library.t) =
dir_internal t
(match lib.public with
| Some { name; _ } -> Public name
| None -> Private (lib.name, lib.scope_name))
let alias = Alias.make ".doc-all"
let deps t =
Build.dyn_paths (Build.arr (
List.fold_left ~init:[] ~f:(fun acc (lib : Lib.t) ->
if Lib.is_local lib then (
let dir =
dir_internal t
(match Lib.status lib with
| Installed -> assert false
| Public -> Public (Lib.name lib)
| Private s -> Private (Lib.name lib, s))
in
Alias.stamp_file (alias ~dir) :: acc
) else (
acc
)
)))
let alias t lib = alias ~dir:(dir t lib)
let static_deps t lib = Alias.dep (alias t lib)
let setup_deps t lib files = add_alias_deps t (alias t lib) files
let dir t lib = dir t lib
end
module Deps = struct
open Build.O
open Dep_conf
let make_alias t ~scope ~dir s =
Alias.of_path (Path.relative dir (expand_vars t ~scope ~dir s))
let dep t ~scope ~dir = function
| File s ->
let path = Path.relative dir (expand_vars t ~scope ~dir s) in
Build.path path
>>^ fun () -> [path]
| Alias s ->
Alias.dep (make_alias t ~scope ~dir s)
>>^ fun () -> []
| Alias_rec s ->
Alias.dep_rec ~loc:(String_with_vars.loc s) ~file_tree:t.file_tree
(make_alias t ~scope ~dir s)
>>^ fun () -> []
| Glob_files s -> begin
let path = Path.relative dir (expand_vars t ~scope ~dir s) in
let loc = String_with_vars.loc s in
match Glob_lexer.parse_string (Path.basename path) with
| Ok re ->
let dir = Path.parent path in
Build.paths_glob ~loc ~dir (Re.compile re)
| Error (_pos, msg) ->
Loc.fail loc "invalid glob: %s" msg
end
| Files_recursively_in s ->
let path = Path.relative dir (expand_vars t ~scope ~dir s) in
Build.files_recursively_in ~dir:path ~file_tree:t.file_tree
>>^ Pset.elements
let interpret t ~scope ~dir l =
Build.all (List.map l ~f:(dep t ~scope ~dir))
>>^ List.concat
end
module Pkg_version = struct
open Build.O
module V = Vfile_kind.Make(struct type t = string option end)
(functor (C : Sexp.Combinators) -> struct
let t = C.option C.string
end)
let spec sctx (p : Package.t) =
let fn =
Path.relative (Path.append sctx.context.build_dir p.path)
(sprintf "%s.version.sexp" p.name)
in
Build.Vspec.T (fn, (module V))
let read sctx p = Build.vpath (spec sctx p)
let set sctx p get =
let spec = spec sctx p in
add_rule sctx (get >>> Build.store_vfile spec);
Build.vpath spec
end
let parse_bang var : bool * string =
let len = String.length var in
if len > 0 && var.[0] = '!' then
(true, String.sub var ~pos:1 ~len:(len - 1))
else
(false, var)
module Action = struct
open Build.O
module U = Action.Unexpanded
type targets =
| Static of Path.t list
| Infer
| Alias
type resolved_forms =
{ (* Failed resolutions *)
mutable failures : fail list
; (* All "name" for ${lib:name:...}/${lib-available:name} forms *)
mutable lib_deps : Build.lib_deps
; (* Static deps from ${...} variables. For instance ${exe:...} *)
mutable sdeps : Pset.t
; (* Dynamic deps from ${...} variables. For instance ${read:...} *)
mutable ddeps : (unit, Action.Var_expansion.t) Build.t String_map.t
}
let add_lib_dep acc lib kind =
acc.lib_deps <- String_map.add acc.lib_deps ~key:lib ~data:kind
let add_fail acc fail =
acc.failures <- fail :: acc.failures;
None
let add_ddep acc ~key dep =
acc.ddeps <- String_map.add acc.ddeps ~key ~data:dep;
None
let path_exp path = Action.Var_expansion.Paths ([path], Concat)
let str_exp path = Action.Var_expansion.Strings ([path], Concat)
(* Static expansion that creates a dependency on the expanded path *)
let static_dep_exp acc path =
acc.sdeps <- Pset.add path acc.sdeps;
Some (path_exp path)
let map_exe sctx =
match sctx.host with
| None -> (fun exe -> exe)
| Some host ->
fun exe ->
match Path.extract_build_context_dir exe with
| Some (dir, exe) when dir = sctx.context.build_dir ->
Path.append host.context.build_dir exe
| _ -> exe
let parse_lib_file ~loc s =
match String.lsplit2 s ~on:':' with
| None ->
Loc.fail loc "invalid ${lib:...} form: %s" s
| Some x -> x
let expand_step1 sctx ~dir ~dep_kind ~scope ~targets_written_by_user ~map_exe t =
let acc =
{ failures = []
; lib_deps = String_map.empty
; sdeps = Pset.empty
; ddeps = String_map.empty
}
in
let t =
U.partial_expand t ~dir ~map_exe ~f:(fun loc key ->
let open Action.Var_expansion in
let has_bang, var = parse_bang key in
if has_bang then
Loc.warn loc "The use of the variable prefix '!' is deprecated, \
simply use '${%s}'@." var;
match String.lsplit2 var ~on:':' with
| Some ("path-no-dep", s) -> Some (path_exp (Path.relative dir s))
| Some ("exe" , s) ->
let exe = map_exe (Path.relative dir s) in
static_dep_exp acc exe
| Some ("path" , s) -> static_dep_exp acc (Path.relative dir s)
| Some ("bin" , s) -> begin
let sctx = host_sctx sctx in
match Artifacts.binary (artifacts sctx) s with
| Ok path ->
static_dep_exp acc path
| Error e ->
add_fail acc ({ fail = fun () -> Action.Prog.Not_found.raise e })
end
(* "findlib" for compatibility with Jane Street packages which are not yet updated
to convert "findlib" to "lib" *)
| Some (("lib"|"findlib"), s) -> begin
let lib_dep, file = parse_lib_file ~loc s in
add_lib_dep acc lib_dep dep_kind;
match
Artifacts.file_of_lib (artifacts sctx) ~loc ~lib:lib_dep ~file
with
| Ok path -> static_dep_exp acc path
| Error fail -> add_fail acc fail
end
| Some ("libexec" , s) -> begin
let sctx = host_sctx sctx in
let lib_dep, file = parse_lib_file ~loc s in
add_lib_dep acc lib_dep dep_kind;
match
Artifacts.file_of_lib (artifacts sctx) ~loc ~lib:lib_dep ~file
with
| Error fail -> add_fail acc fail
| Ok path ->
if not Sys.win32 || Filename.extension s = ".exe" then begin
static_dep_exp acc path
end else begin
let path_exe = Path.extend_basename path ~suffix:".exe" in
let dep =
Build.if_file_exists path_exe
~then_:(Build.path path_exe >>^ fun _ -> path_exp path_exe)
~else_:(Build.path path >>^ fun _ -> path_exp path)
in
add_ddep acc ~key dep
end
end
| Some ("lib-available", lib) ->
add_lib_dep acc lib Optional;
Some (str_exp (string_of_bool (
Lib.DB.available (Scope.libs scope) lib)))
| Some ("version", s) -> begin
match Scope_info.resolve (Scope.info scope) s with
| Ok p ->
let x =
Pkg_version.read sctx p >>^ function
| None -> Strings ([""], Concat)
| Some s -> Strings ([s], Concat)
in
add_ddep acc ~key x
| Error s ->
add_fail acc { fail = fun () -> Loc.fail loc "%s" s }
end
| Some ("read", s) -> begin
let path = Path.relative dir s in
let data =
Build.contents path
>>^ fun s -> Strings ([s], Concat)
in
add_ddep acc ~key data
end
| Some ("read-lines", s) -> begin
let path = Path.relative dir s in
let data =
Build.lines_of path
>>^ fun l -> Strings (l, Split)
in
add_ddep acc ~key data
end
| Some ("read-strings", s) -> begin
let path = Path.relative dir s in
let data =
Build.strings path
>>^ fun l -> Strings (l, Split)
in
add_ddep acc ~key data
end
| _ ->
match var with
| "ROOT" -> Some (path_exp sctx.context.build_dir)
| "SCOPE_ROOT" -> Some (path_exp (Scope.root scope))
| "@" -> begin
match targets_written_by_user with
| Infer -> Loc.fail loc "You cannot use ${@} with inferred rules."
| Alias -> Loc.fail loc "You cannot use ${@} in aliases."
| Static l -> Some (Paths (l, Split))
end
| _ -> expand_var_no_root sctx var)
in
(t, acc)
let expand_step2 ~dir ~dynamic_expansions ~deps_written_by_user ~map_exe t =
let open Action.Var_expansion in
U.Partial.expand t ~dir ~map_exe ~f:(fun loc key ->
match String_map.find key dynamic_expansions with
| Some _ as opt -> opt
| None ->
let _, var = parse_bang key in
match var with
| "<" ->
Some
(match deps_written_by_user with
| [] ->
Loc.warn loc "Variable '<' used with no explicit \
dependencies@.";
Strings ([""], Concat)
| dep :: _ ->
Paths ([dep], Concat))
| "^" -> Some (Paths (deps_written_by_user, Split))
| _ -> None)
let run sctx t ~dir ~dep_kind ~targets:targets_written_by_user ~scope
: (Path.t list, Action.t) Build.t =
let map_exe = map_exe sctx in
if targets_written_by_user = Alias then begin
match Action.Infer.unexpanded_targets t with
| [] -> ()
| x :: _ ->
let loc = String_with_vars.loc x in
Loc.warn loc "Aliases must not have targets, this target will be ignored.\n\
This will become an error in the future.";
end;
let t, forms =
expand_step1 sctx t ~dir ~dep_kind ~scope
~targets_written_by_user ~map_exe
in
let { Action.Infer.Outcome. deps; targets } =
match targets_written_by_user with
| Infer -> Action.Infer.partial t ~all_targets:true
| Static targets_written_by_user ->
let targets_written_by_user = Pset.of_list targets_written_by_user in
let { Action.Infer.Outcome. deps; targets } =
Action.Infer.partial t ~all_targets:false
in
(* CR-someday jdimino: should this be an error or not?
It's likely that what we get here is what the user thinks of as temporary
files, even though they might conflict with actual targets. We need to tell
jbuilder about such things, so that it can report better errors.
{[
let missing = Pset.diff targets targets_written_by_user in
if not (Pset.is_empty missing) then
Loc.warn (Loc.in_file (Utils.jbuild_name_in ~dir))
"Missing targets in user action:\n%s"
(List.map (Pset.elements missing) ~f:(fun target ->
sprintf "- %s" (Utils.describe_target target))
|> String.concat ~sep:"\n");
]}
*)
{ deps; targets = Pset.union targets targets_written_by_user }
| Alias ->
let { Action.Infer.Outcome. deps; targets = _ } =
Action.Infer.partial t ~all_targets:false
in
{ deps; targets = Pset.empty }
in
let targets = Pset.elements targets in
List.iter targets ~f:(fun target ->
if Path.parent target <> dir then
Loc.fail (Loc.in_file (Utils.describe_target (Utils.jbuild_file_in ~dir)))
"A rule in this jbuild has targets in a different directory \
than the current one, this is not allowed by Jbuilder at the moment:\n%s"
(List.map targets ~f:(fun target ->
sprintf "- %s" (Utils.describe_target target))
|> String.concat ~sep:"\n"));
let build =
Build.record_lib_deps_simple forms.lib_deps
>>>
Build.path_set deps
>>>
Build.path_set forms.sdeps
>>>
Build.arr (fun paths -> ((), paths))
>>>
let ddeps = String_map.bindings forms.ddeps in
Build.first (Build.all (List.map ddeps ~f:snd))
>>^ (fun (vals, deps_written_by_user) ->
let dynamic_expansions =
List.fold_left2 ddeps vals ~init:String_map.empty ~f:(fun acc (var, _) value ->
String_map.add acc ~key:var ~data:value)
in
let unresolved =
expand_step2 t ~dir ~dynamic_expansions ~deps_written_by_user ~map_exe
in
Action.Unresolved.resolve unresolved ~f:(fun prog ->
let sctx = host_sctx sctx in
match Artifacts.binary sctx.artifacts prog with
| Ok path -> path
| Error fail -> Action.Prog.Not_found.raise fail))
>>>
Build.dyn_paths (Build.arr (fun action ->
let { Action.Infer.Outcome.deps; targets = _ } =
Action.Infer.infer action
in
Pset.elements deps))
>>>
Build.action_dyn () ~dir ~targets
in
match forms.failures with
| [] -> build
| fail :: _ -> Build.fail fail >>> build
end
module PP = struct
open Build.O
let pp_fname fn =
let fn, ext = Filename.split_extension fn in
(* We need to to put the .pp before the .ml so that the compiler realises that
[foo.pp.mli] is the interface for [foo.pp.ml] *)
fn ^ ".pp" ^ ext
let pped_module ~dir (m : Module.t) ~f =
let pped_file (kind : Ml_kind.t) (file : Module.File.t) =
let pp_fname = pp_fname file.name in
f kind (Path.relative dir file.name) (Path.relative dir pp_fname);
{file with name = pp_fname}
in
{ m with
impl = Option.map m.impl ~f:(pped_file Impl)
; intf = Option.map m.intf ~f:(pped_file Intf)
}
let migrate_driver_main = "ocaml-migrate-parsetree.driver-main"
let build_ppx_driver sctx ~lib_db ~dep_kind ~target pps =
let ctx = sctx.context in
let mode = Context.best_mode ctx in
let compiler = Option.value_exn (Context.compiler ctx mode) in
let pps = pps @ [Pp.of_string migrate_driver_main] in
let driver, libs =
let resolved_pps = Lib.DB.resolve_pps lib_db pps in
let driver =
match resolved_pps with
| Ok l -> List.last l
| Error _ -> None
in
(driver,
Result.bind resolved_pps ~f:Lib.closure
|> Libs.requires_to_build
~required_by:[Preprocess (pps : Jbuild.Pp.t list :> string list)])
in
let libs =
Build.record_lib_deps ~kind:dep_kind (List.map pps ~f:Lib_dep.of_pp)
>>>
libs
in
let libs =
(* Put the driver back at the end, just before migrate_driver_main *)
match driver with
| None -> libs
| Some driver ->
libs >>^ fun libs ->
let libs, drivers =
List.partition_map libs ~f:(fun lib ->
if lib == driver || Lib.name lib = migrate_driver_main then
Inr lib
else
Inl lib)
in
let user_driver, migrate_driver =
List.partition_map drivers ~f:(fun lib ->
if Lib.name lib = migrate_driver_main then
Inr lib
else
Inl lib)
in
libs @ user_driver @ migrate_driver
in
(* Provide a better error for migrate_driver_main given that this is an implicit
dependency *)
let libs =
match Lib.DB.available lib_db migrate_driver_main with
| false ->
Build.fail { fail = fun () ->
die "@{<error>Error@}: I couldn't find '%s'.\n\
I need this library in order to use ppx rewriters.\n\
See the manual for details.\n\
Hint: opam install ocaml-migrate-parsetree"
migrate_driver_main
}
>>>
libs
| true ->
libs
in
add_rule sctx
(libs
>>>
Build.dyn_paths
(Build.arr
(Lib.L.archive_files ~mode ~ext_lib:ctx.ext_lib))
>>>
Build.run ~context:ctx (Ok compiler)
[ A "-o" ; Target target
; Dyn (Lib.L.link_flags ~mode ~stdlib_dir:ctx.stdlib_dir)
])
let gen_rules sctx components =
match components with
| [key] ->
let ppx_dir = Path.relative sctx.ppx_dir key in
let exe = Path.relative ppx_dir "ppx.exe" in
let (key, lib_db) =
match String.rsplit2 key ~on:'@' with
| None ->
(key, sctx.public_libs)
| Some (key, scope) ->
(key, Scope.libs (find_scope_by_name sctx
(Scope_info.Name.of_string scope)))
in
let names =
match key with
| "+none+" -> []
| _ -> String.split key ~on:'+'
in
let names =
match List.rev names with
| [] -> []
| driver :: rest -> List.sort rest ~cmp:String.compare @ [driver]
in
let pps = List.map names ~f:Jbuild.Pp.of_string in
build_ppx_driver sctx pps ~lib_db ~dep_kind:Required ~target:exe
| _ -> ()
let most_specific_db (a : Lib.Status.t) (b : Lib.Status.t) =
match a, b with
| Private x, Private y -> assert (x = y); a
| Private _, _ -> a
| _ , Private _ -> b
| Public , _
| _ , Public -> Public
| Installed, Installed -> Installed
let get_ppx_driver sctx ~scope pps =
let driver, names =
match List.rev_map pps ~f:Pp.to_string with
| [] -> (None, [])
| driver :: rest -> (Some driver, rest)
in
let sctx = host_sctx sctx in
let name_and_db name =
match Lib.DB.find (Scope.libs scope) name with
| Error _ ->
(* XXX unknown but assume it's public *)
(name, Lib.Status.Installed)
| Ok lib ->
(Lib.name lib, Lib.status lib)
in
let driver, driver_db =
match driver with
| None -> (None, Lib.Status.Installed)
| Some driver ->
let name, db = name_and_db driver in
(Some name, db)
in
let names, db =
List.fold_left names ~init:([], driver_db) ~f:(fun (names, db) lib ->
let name, db' = name_and_db lib in
(name :: names, most_specific_db db db'))
in
let names = List.sort ~cmp:String.compare names in
let names =
match driver with
| None -> names
| Some driver -> names @ [driver]
in
let key =
match names with
| [] -> "+none+"
| _ -> String.concat names ~sep:"+"
in
let key =
match db with
| Installed | Public -> key
| Private scope_name ->
sprintf "%s@%s" key (Scope_info.Name.to_string scope_name)
in
let sctx = host_sctx sctx in
let ppx_dir = Path.relative sctx.ppx_dir key in
Path.relative ppx_dir "ppx.exe"
let target_var = String_with_vars.virt_var __POS__ "@"
let root_var = String_with_vars.virt_var __POS__ "ROOT"
let cookie_library_name lib_name =
match lib_name with
| None -> []
| Some name -> ["--cookie"; sprintf "library-name=%S" name]
(* Generate rules for the reason modules in [modules] and return a
a new module with only OCaml sources *)
let setup_reason_rules sctx ~dir (m : Module.t) =
let ctx = sctx.context in
let refmt =
Artifacts.binary sctx.artifacts "refmt" ~hint:"opam install reason" in
let rule src target =
let src_path = Path.relative dir src in
Build.run ~context:ctx refmt
[ A "--print"
; A "binary"
; Dep src_path ]
~stdout_to:(Path.relative dir target) in
let to_ml (f : Module.File.t) =
match f.syntax with
| OCaml -> f
| Reason ->
let ml = Module.File.to_ocaml f in
add_rule sctx (rule f.name ml.name);
ml
in
{ m with
impl = Option.map m.impl ~f:to_ml
; intf = Option.map m.intf ~f:to_ml
}
let uses_ppx_driver ~pps =
match Option.map ~f:Pp.to_string (List.last pps) with
| Some ("ppx_driver.runner" | "ppx_base.runner") -> true
| Some _ | None -> false
let promote_correction ~uses_ppx_driver fn build =
if not uses_ppx_driver then
build
else
Build.progn
[ build
; Build.return
(A.diff ~optional:true
fn
(Path.extend_basename fn ~suffix:".ppx-corrected"))
]
let lint_module sctx ~dir ~dep_kind ~lint ~lib_name ~scope = Staged.stage (
let alias = Alias.lint ~dir in
let add_alias fn build =
Alias.add_action sctx.build_system alias build
~stamp:(List [ Sexp.unsafe_atom_of_string "lint"
; Sexp.To_sexp.(option string) lib_name
; Sexp.atom fn
])
in
let lint =
Per_module.map lint ~f:(function
| Preprocess.No_preprocessing ->
(fun ~source:_ ~ast:_ -> ())
| Action action ->
(fun ~source ~ast:_ ->
let action = Action.U.Chdir (root_var, action) in
Module.iter source ~f:(fun _ (src : Module.File.t) ->
let src_path = Path.relative dir src.name in
add_alias src.name
(Build.path src_path
>>^ (fun _ -> [src_path])
>>> Action.run sctx
action
~dir
~dep_kind
~targets:(Static [])
~scope)))
| Pps { pps; flags } ->
let ppx_exe = get_ppx_driver sctx ~scope pps in
let uses_ppx_driver = uses_ppx_driver ~pps in
let args : _ Arg_spec.t =
S [ As flags
; As (cookie_library_name lib_name)
(* This hack is needed until -null is standard:
https://github.com/ocaml-ppx/ocaml-migrate-parsetree/issues/35
*)
; As (if uses_ppx_driver then
[ "-null"; "-diff-cmd"; "-" ]
else
[])
]
in
(fun ~source ~ast ->
Module.iter ast ~f:(fun kind src ->
let args =
[ args
; Ml_kind.ppx_driver_flag kind
; Dep (Path.relative dir src.name)
]
in
add_alias src.name
(promote_correction ~uses_ppx_driver
(Option.value_exn (Module.file ~dir source kind))
(Build.run ~context:sctx.context (Ok ppx_exe) args))
)))
in
fun ~(source : Module.t) ~ast ->
Per_module.get lint source.name ~source ~ast)
(* Generate rules to build the .pp files and return a new module map
where all filenames point to the .pp files *)
let pp_and_lint_modules sctx ~dir ~dep_kind ~modules ~lint ~preprocess
~preprocessor_deps ~lib_name ~scope =
let preprocessor_deps =
Build.memoize "preprocessor deps" preprocessor_deps
in
let lint_module =
Staged.unstage (lint_module sctx ~dir ~dep_kind ~lint ~lib_name ~scope)
in
let preprocess =
Per_module.map preprocess ~f:(function
| Preprocess.No_preprocessing ->
(fun m ->
let ast = setup_reason_rules sctx ~dir m in
lint_module ~ast ~source:m;
ast)
| Action action ->
(fun m ->
let ast =
pped_module m ~dir ~f:(fun _kind src dst ->
add_rule sctx
(preprocessor_deps
>>>
Build.path src
>>^ (fun _ -> [src])
>>>
Action.run sctx
(Redirect
(Stdout,
target_var,
Chdir (root_var,
action)))
~dir
~dep_kind
~targets:(Static [dst])
~scope))
|> setup_reason_rules sctx ~dir in
lint_module ~ast ~source:m;
ast)
| Pps { pps; flags } ->
let ppx_exe = get_ppx_driver sctx ~scope pps in
let uses_ppx_driver = uses_ppx_driver ~pps in
let args : _ Arg_spec.t =
S [ As flags
; A "--dump-ast"
; As (cookie_library_name lib_name)
; As (if uses_ppx_driver then ["-diff-cmd"; "-"] else [])
]
in
(fun m ->
let ast = setup_reason_rules sctx ~dir m in
lint_module ~ast ~source:m;
pped_module ast ~dir ~f:(fun kind src dst ->
add_rule sctx
(promote_correction ~uses_ppx_driver
(Option.value_exn (Module.file m ~dir kind))
(preprocessor_deps
>>>
Build.run ~context:sctx.context
(Ok ppx_exe)
[ args
; A "-o"; Target dst
; Ml_kind.ppx_driver_flag kind; Dep src
])))))
in
String_map.map modules ~f:(fun (m : Module.t) ->
Per_module.get preprocess m.name m)
end
module Eval_strings = Ordered_set_lang.Make(struct
type t = string
let name t = t
end)
let expand_and_eval_set t ~scope ~dir set ~standard =
let open Build.O in
let f = expand_vars t ~scope ~dir in
let parse ~loc:_ s = s in
match Ordered_set_lang.Unexpanded.files set ~f |> String_set.elements with
| [] ->
let set = Ordered_set_lang.Unexpanded.expand set ~files_contents:String_map.empty ~f in
Build.return (Eval_strings.eval set ~standard ~parse)
| files ->
let paths = List.map files ~f:(Path.relative dir) in
Build.all (List.map paths ~f:Build.read_sexp)
>>^ fun sexps ->
let files_contents = List.combine files sexps |> String_map.of_alist_exn in
let set = Ordered_set_lang.Unexpanded.expand set ~files_contents ~f in
Eval_strings.eval set ~standard ~parse