dune/src/build_system.ml

818 lines
26 KiB
OCaml

open Import
open Future
module Pset = Path.Set
module Pmap = Path.Map
module Vspec = Build.Vspec
module Exec_status = struct
module Starting = struct
type t = { for_file : Path.t }
end
module Evaluating_rule = struct
type t =
{ for_file : Path.t
; rule_evaluation : (Action.t * Pset.t) Future.t
; exec_rule : targeting:Path.t
-> (Action.t * Pset.t) Future.t -> unit Future.t
}
end
module Running = struct
type t =
{ for_file : Path.t
; (* Future that only waits for the evaluation of the rule to terminate. It holds
the computed action and dynamic dependencies. *)
rule_evaluation : (Action.t * Pset.t) Future.t
; (* Future that waits for the rule's action to terminate *)
rule_execution : unit Future.t
}
end
module Not_started = struct
type t =
{ eval_rule : targeting:Path.t -> (Action.t * Pset.t) Future.t
; exec_rule : targeting:Path.t -> (Action.t * Pset.t) Future.t -> unit Future.t
}
end
type t =
| Not_started of Not_started.t
| Starting of Starting.t
| Evaluating_rule of Evaluating_rule.t
| Running of Running.t
end
module Internal_rule = struct
module Id : sig
type t
val to_int : t -> int
val compare : t -> t -> int
val gen : unit -> t
end = struct
type t = int
let to_int x = x
let compare (x : int) y = compare x y
let counter = ref 0
let gen () =
let n = !counter in
counter := n + 1;
n
end
type t =
{ id : Id.t
; rule_deps : Pset.t
; static_deps : Pset.t
; targets : Pset.t
; context : Context.t option
; build : (unit, Action.t) Build.t
; mutable exec : Exec_status.t
}
let compare a b = Id.compare a.id b.id
end
module File_kind = struct
type 'a t =
| Ignore_contents : unit t
| Sexp_file : 'a Vfile_kind.t -> 'a t
let eq : type a b. a t -> b t -> (a, b) eq option = fun a b ->
match a, b with
| Ignore_contents, Ignore_contents -> Some Eq
| Sexp_file a , Sexp_file b -> Vfile_kind.eq a b
| _ -> None
let eq_exn a b = Option.value_exn (eq a b)
end
module File_spec = struct
type 'a t =
{ rule : Internal_rule.t (* Rule which produces it *)
; mutable kind : 'a File_kind.t
; mutable data : 'a option
}
type packed = T : _ t -> packed
let create rule kind =
T { rule; kind; data = None }
end
type t =
{ (* File specification by targets *)
files : (Path.t, File_spec.packed) Hashtbl.t
; contexts : Context.t list
; (* Table from target to digest of [(deps, targets, action)] *)
trace : (Path.t, Digest.t) Hashtbl.t
; timestamps : (Path.t, float) Hashtbl.t
; mutable local_mkdirs : Path.Local.Set.t
}
let all_targets t = Hashtbl.fold t.files ~init:[] ~f:(fun ~key ~data:_ acc -> key :: acc)
let timestamp t fn =
match Hashtbl.find t.timestamps fn with
| Some _ as x -> x
| None ->
match Unix.lstat (Path.to_string fn) with
| exception _ -> None
| stat ->
let ts = stat.st_mtime in
Hashtbl.add t.timestamps ~key:fn ~data:ts;
Some ts
type limit_timestamp =
{ missing_files : bool
; limit : float option
}
let merge_timestamp t fns ~merge =
let init =
{ missing_files = false
; limit = None
}
in
List.fold_left fns ~init
~f:(fun acc fn ->
match timestamp t fn with
| None -> { acc with missing_files = true }
| Some ts ->
{ acc with
limit =
match acc.limit with
| None -> Some ts
| Some ts' -> Some (merge ts ts')
})
let min_timestamp t fns = merge_timestamp t fns ~merge:min
let max_timestamp t fns = merge_timestamp t fns ~merge:max
let find_file_exn t file =
Hashtbl.find_exn t.files file ~string_of_key:(fun fn -> sprintf "%S" (Path.to_string fn))
~table_desc:(fun _ -> "<target to rule>")
let is_target t file = Hashtbl.mem t.files file
module Build_error = struct
type t =
{ backtrace : Printexc.raw_backtrace
; dep_path : Path.t list
; exn : exn
}
let backtrace t = t.backtrace
let dependency_path t = t.dep_path
let exn t = t.exn
exception E of t
let raise t ~targeting ~backtrace exn =
let rec build_path acc targeting ~seen =
assert (not (Pset.mem targeting seen));
let seen = Pset.add targeting seen in
let (File_spec.T file) = find_file_exn t targeting in
match file.rule.exec with
| Not_started _ -> assert false
| Running { for_file; _ } | Starting { for_file }
| Evaluating_rule { for_file; _ } ->
if for_file = targeting then
acc
else
build_path (for_file :: acc) for_file ~seen
in
let dep_path = build_path [targeting] targeting ~seen:Pset.empty in
raise (E { backtrace; dep_path; exn })
end
let wrap_build_errors t ~f ~targeting =
with_exn_handler (fun () -> f ~targeting)
~handler:(fun exn backtrace ->
match exn with
| Build_error.E _ -> reraise exn
| exn -> Build_error.raise t exn ~targeting ~backtrace)
let wait_for_file t fn ~targeting =
match Hashtbl.find t.files fn with
| None ->
if Path.is_in_build_dir fn then
die "no rule found for %s" (Utils.describe_target fn)
else if Path.exists fn then
return ()
else
die "file unavailable: %s" (Path.to_string fn)
| Some (File_spec.T file) ->
match file.rule.exec with
| Not_started { eval_rule; exec_rule } ->
file.rule.exec <- Starting { for_file = targeting };
let rule_evaluation =
wrap_build_errors t ~targeting:fn ~f:eval_rule
in
let rule_execution =
wrap_build_errors t ~targeting:fn ~f:(exec_rule rule_evaluation)
in
file.rule.exec <-
Running { for_file = targeting
; rule_evaluation
; rule_execution
};
rule_execution
| Running { rule_execution; _ } -> rule_execution
| Evaluating_rule { for_file; rule_evaluation; exec_rule } ->
file.rule.exec <- Starting { for_file = targeting };
let rule_execution =
wrap_build_errors t ~targeting:fn ~f:(exec_rule rule_evaluation)
in
file.rule.exec <-
Running { for_file
; rule_evaluation
; rule_execution
};
rule_execution
| Starting _ ->
(* Recursive deps! *)
let rec build_loop acc targeting =
let acc = targeting :: acc in
if fn = targeting then
acc
else
let (File_spec.T file) = find_file_exn t targeting in
match file.rule.exec with
| Not_started _ | Running _ | Evaluating_rule _ -> assert false
| Starting { for_file } ->
build_loop acc for_file
in
let loop = build_loop [fn] targeting in
die "Dependency cycle between the following files:\n %s"
(String.concat ~sep:"\n--> "
(List.map loop ~f:Path.to_string))
module Target = Build_interpret.Target
let get_file : type a. t -> Path.t -> a File_kind.t -> a File_spec.t = fun t fn kind ->
match Hashtbl.find t.files fn with
| None -> die "no rule found for %s" (Path.to_string fn)
| Some (File_spec.T file) ->
let Eq = File_kind.eq_exn kind file.kind in
file
let vfile_to_string (type a) (module K : Vfile_kind.S with type t = a) fn x =
K.to_string fn x
module Build_exec = struct
open Build.Repr
let exec bs t x =
let rec exec
: type a b. Pset.t ref -> (a, b) t -> a -> b = fun dyn_deps t x ->
match t with
| Arr f -> f x
| Targets _ -> x
| Store_vfile (Vspec.T (fn, kind)) ->
let file = get_file bs fn (Sexp_file kind) in
file.data <- Some x;
Update_file (fn, vfile_to_string kind fn x)
| Compose (a, b) ->
exec dyn_deps a x |> exec dyn_deps b
| First t ->
let x, y = x in
(exec dyn_deps t x, y)
| Second t ->
let x, y = x in
(x, exec dyn_deps t y)
| Split (a, b) ->
let x, y = x in
let x = exec dyn_deps a x in
let y = exec dyn_deps b y in
(x, y)
| Fanout (a, b) ->
let a = exec dyn_deps a x in
let b = exec dyn_deps b x in
(a, b)
| Paths _ -> x
| Paths_glob _ -> x
| Contents p -> Io.read_file (Path.to_string p)
| Lines_of p -> Io.lines_of_file (Path.to_string p)
| Vpath (Vspec.T (fn, kind)) ->
let file : b File_spec.t = get_file bs fn (Sexp_file kind) in
Option.value_exn file.data
| Dyn_paths t ->
let fns = exec dyn_deps t x in
dyn_deps := Pset.union !dyn_deps (Pset.of_list fns);
x
| Record_lib_deps _ -> x
| Fail { fail } -> fail ()
| If_file_exists (_, state) ->
exec dyn_deps (get_if_file_exists_exn state) x
| Memo m ->
match m.state with
| Evaluated (x, deps) ->
dyn_deps := Pset.union !dyn_deps deps;
x
| Evaluating ->
die "Dependency cycle evaluating memoized build arrow %s" m.name
| Unevaluated ->
m.state <- Evaluating;
let dyn_deps' = ref Pset.empty in
let x = exec dyn_deps' m.t x in
m.state <- Evaluated (x, !dyn_deps');
dyn_deps := Pset.union !dyn_deps !dyn_deps';
x
in
let dyn_deps = ref Pset.empty in
let action = exec dyn_deps (Build.repr t) x in
(action, !dyn_deps)
end
let add_spec t fn spec ~allow_override =
if not allow_override && Hashtbl.mem t.files fn then
die "multiple rules generated for %s" (Path.to_string fn);
Hashtbl.add t.files ~key:fn ~data:spec
let create_file_specs t targets rule ~allow_override =
List.iter targets ~f:(function
| Target.Normal fn ->
add_spec t fn (File_spec.create rule Ignore_contents) ~allow_override
| Target.Vfile (Vspec.T (fn, kind)) ->
add_spec t fn (File_spec.create rule (Sexp_file kind)) ~allow_override)
module Pre_rule = Build_interpret.Rule
let refresh_targets_timestamps_after_rule_execution t targets =
let missing =
List.fold_left targets ~init:Pset.empty ~f:(fun acc fn ->
match Unix.lstat (Path.to_string fn) with
| exception _ -> Pset.add fn acc
| stat ->
let ts = stat.st_mtime in
Hashtbl.replace t.timestamps ~key:fn ~data:ts;
acc)
in
if not (Pset.is_empty missing) then
die "@{<error>Error@}: Rule failed to generate the following targets:\n%s"
(Pset.elements missing
|> List.map ~f:(fun fn -> sprintf "- %s" (Path.to_string fn))
|> String.concat ~sep:"\n")
let wait_for_deps t deps ~targeting =
all_unit
(Pset.fold deps ~init:[] ~f:(fun fn acc -> wait_for_file t fn ~targeting :: acc))
(* This contains the targets of the actions that are being executed. On exit, we need to
delete them as they might contain garbage *)
let pending_targets = ref Pset.empty
let () =
Future.Scheduler.at_exit_after_waiting_for_commands (fun () ->
let fns = !pending_targets in
pending_targets := Pset.empty;
Pset.iter fns ~f:Path.unlink_no_err)
let make_local_dirs t paths =
Pset.iter paths ~f:(fun path ->
match Path.kind path with
| Local path ->
if not (Path.Local.Set.mem path t.local_mkdirs) then begin
Path.Local.mkdir_p path;
t.local_mkdirs <- Path.Local.Set.add path t.local_mkdirs
end
| _ -> ())
let make_local_parent_dirs t paths ~map_path =
Pset.iter paths ~f:(fun path ->
match Path.kind (map_path path) with
| Local path when not (Path.Local.is_root path) ->
let parent = Path.Local.parent path in
if not (Path.Local.Set.mem parent t.local_mkdirs) then begin
Path.Local.mkdir_p parent;
t.local_mkdirs <- Path.Local.Set.add parent t.local_mkdirs
end
| _ -> ())
let sandbox_dir = Path.of_string "_build/.sandbox"
let compile_rule t ~all_targets_by_dir ?(allow_override=false) pre_rule =
let { Pre_rule. context; build; targets = target_specs; sandbox } = pre_rule in
let targets = Target.paths target_specs in
let { Build_interpret.Static_deps.
rule_deps
; action_deps = static_deps
} = Build_interpret.static_deps build ~all_targets_by_dir
in
let eval_rule ~targeting =
wait_for_deps t rule_deps ~targeting
>>| fun () ->
Build_exec.exec t build ()
in
let exec_rule ~targeting rule_evaluation =
make_local_parent_dirs t targets ~map_path:(fun x -> x);
Future.both
(wait_for_deps t static_deps ~targeting)
(rule_evaluation >>= fun (action, dyn_deps) ->
wait_for_deps t ~targeting (Pset.diff dyn_deps static_deps)
>>| fun () ->
(action, dyn_deps))
>>= fun ((), (action, dyn_deps)) ->
let all_deps = Pset.union static_deps dyn_deps in
let all_deps_as_list = Pset.elements all_deps in
let targets_as_list = Pset.elements targets in
let hash =
let trace =
(all_deps_as_list,
targets_as_list,
Option.map context ~f:(fun c -> c.name),
action)
in
Digest.string (Marshal.to_string trace [])
in
let sandbox_dir =
if sandbox then
Some (Path.relative sandbox_dir (Digest.to_hex hash))
else
None
in
let rule_changed =
List.fold_left targets_as_list ~init:false ~f:(fun acc fn ->
match Hashtbl.find t.trace fn with
| None ->
Hashtbl.add t.trace ~key:fn ~data:hash;
true
| Some prev_hash ->
Hashtbl.replace t.trace ~key:fn ~data:hash;
acc || prev_hash <> hash)
in
let targets_min_ts = min_timestamp t targets_as_list in
let deps_max_ts = max_timestamp t all_deps_as_list in
if rule_changed ||
match deps_max_ts, targets_min_ts with
| _, { missing_files = true; _ } ->
(* Missing targets -> rebuild *)
true
| _, { missing_files = false; limit = None } ->
(* CR-someday jdimino: no target, this should be a user error *)
true
| { missing_files = true; _ }, _ ->
Sexp.code_error
"Dependencies missing after waiting for them"
[ "all_deps", Sexp.To_sexp.list Path.sexp_of_t all_deps_as_list ]
| { limit = None; missing_files = false },
{ missing_files = false; _ } ->
(* No dependencies, no need to do anything if the rule hasn't changed and targets
are here. *)
false
| { limit = Some deps_max; missing_files = false },
{ limit = Some targets_min; missing_files = false } ->
targets_min < deps_max
then (
(* Do not remove files that are just updated, otherwise this would break incremental
compilation *)
let targets_to_remove =
Pset.diff targets (Action.updated_files action)
in
Pset.iter targets_to_remove ~f:Path.unlink_no_err;
pending_targets := Pset.union targets_to_remove !pending_targets;
let action =
match sandbox_dir with
| Some sandbox_dir ->
Path.rm_rf sandbox_dir;
let sandboxed path =
if Path.is_local path then
Path.append sandbox_dir path
else
path
in
make_local_parent_dirs t all_deps ~map_path:sandboxed;
make_local_parent_dirs t targets ~map_path:sandboxed;
Action.sandbox action
~sandboxed
~deps:all_deps_as_list
~targets:targets_as_list
| None ->
action
in
make_local_dirs t (Action.chdirs action);
Action.exec ~targets action >>| fun () ->
Option.iter sandbox_dir ~f:Path.rm_rf;
(* All went well, these targets are no longer pending *)
pending_targets := Pset.diff !pending_targets targets_to_remove;
refresh_targets_timestamps_after_rule_execution t targets_as_list
) else
return ()
in
let rule =
{ Internal_rule.
id = Internal_rule.Id.gen ()
; static_deps
; rule_deps
; targets
; build
; context
; exec = Not_started { eval_rule; exec_rule }
}
in
create_file_specs t target_specs rule ~allow_override
let setup_copy_rules t ~all_non_target_source_files ~all_targets_by_dir =
List.iter t.contexts ~f:(fun (ctx : Context.t) ->
let ctx_dir = ctx.build_dir in
Pset.iter all_non_target_source_files ~f:(fun path ->
let ctx_path = Path.append ctx_dir path in
if is_target t ctx_path &&
String.is_suffix (Path.basename ctx_path) ~suffix:".install" then
(* Do not copy over .install files that are generated by a rule. *)
()
else
let build = Build.copy ~src:path ~dst:ctx_path in
(* We temporarily allow overrides while setting up copy rules
from the source directory so that artifact that are already
present in the source directory are not re-computed.
This allows to keep generated files in tarballs. Maybe we
should allow it on a case-by-case basis though. *)
compile_rule t (Pre_rule.make build)
~all_targets_by_dir
~allow_override:true))
module Trace = struct
type t = (Path.t, Digest.t) Hashtbl.t
let file = "_build/.db"
let dump (trace : t) =
let sexp =
Sexp.List (
Hashtbl.fold trace ~init:Pmap.empty ~f:(fun ~key ~data acc ->
Pmap.add acc ~key ~data)
|> Path.Map.bindings
|> List.map ~f:(fun (path, hash) ->
Sexp.List [ Atom (Path.to_string path); Atom (Digest.to_hex hash) ]))
in
if Sys.file_exists "_build" then
Io.write_file file (Sexp.to_string sexp)
let load () =
let trace = Hashtbl.create 1024 in
if Sys.file_exists file then begin
let sexp = Sexp_lexer.Load.single file in
let bindings =
let open Sexp.Of_sexp in
list (pair Path.t (fun s -> Digest.from_hex (string s))) sexp
in
List.iter bindings ~f:(fun (path, hash) ->
Hashtbl.add trace ~key:path ~data:hash);
end;
trace
end
let all_targets_ever_built () =
if Sys.file_exists Trace.file then
let trace = Trace.load () in
Hashtbl.fold trace ~init:[] ~f:(fun ~key ~data:_ acc -> key :: acc)
else
[]
let create ~contexts ~file_tree ~rules =
let all_source_files =
File_tree.fold file_tree ~init:Pset.empty ~f:(fun dir acc ->
let path = File_tree.Dir.path dir in
Cont
(Pset.union acc
(File_tree.Dir.files dir
|> String_set.elements
|> List.map ~f:(Path.relative path)
|> Pset.of_list)))
in
let all_copy_targets =
List.fold_left contexts ~init:Pset.empty ~f:(fun acc (ctx : Context.t) ->
Pset.union acc (Pset.elements all_source_files
|> List.map ~f:(Path.append ctx.build_dir)
|> Pset.of_list))
in
let all_other_targets =
List.fold_left rules ~init:Pset.empty ~f:(fun acc { Pre_rule.targets; _ } ->
List.fold_left targets ~init:acc ~f:(fun acc target ->
Pset.add (Target.path target) acc))
in
let all_targets_by_dir = lazy (
Pset.elements (Pset.union all_copy_targets all_other_targets)
|> List.filter_map ~f:(fun path ->
if Path.is_root path then
None
else
Some (Path.parent path, path))
|> Pmap.of_alist_multi
|> Pmap.map ~f:Pset.of_list
) in
let t =
{ contexts
; files = Hashtbl.create 1024
; trace = Trace.load ()
; timestamps = Hashtbl.create 1024
; local_mkdirs = Path.Local.Set.empty
} in
List.iter rules ~f:(compile_rule t ~all_targets_by_dir ~allow_override:false);
setup_copy_rules t ~all_targets_by_dir
~all_non_target_source_files:
(Pset.diff all_source_files all_other_targets);
at_exit (fun () -> Trace.dump t.trace);
t
let remove_old_artifacts t =
let rec walk dir =
let keep =
if Hashtbl.mem t.files (Path.relative dir Config.jbuilder_keep_fname) then
true
else begin
Path.readdir dir
|> List.filter ~f:(fun fn ->
let fn = Path.relative dir fn in
match Unix.lstat (Path.to_string fn) with
| { st_kind = S_DIR; _ } ->
walk fn
| exception _ ->
let keep = Hashtbl.mem t.files fn in
if not keep then Path.unlink fn;
keep
| _ ->
let keep = Hashtbl.mem t.files fn in
if not keep then Path.unlink fn;
keep)
|> function
| [] -> false
| _ -> true
end
in
if not keep then Path.rmdir dir;
keep
in
let walk dir =
if Path.exists dir then ignore (walk dir : bool)
in
List.iter t.contexts ~f:(fun (ctx : Context.t) ->
walk ctx.build_dir;
walk (Config.local_install_dir ~context:ctx.name);
)
let do_build_exn t targets =
remove_old_artifacts t;
all_unit (List.map targets ~f:(fun fn -> wait_for_file t fn ~targeting:fn))
let do_build t targets =
try
Ok (do_build_exn t targets)
with Build_error.E e ->
Error e
module Ir_set = Set.Make(Internal_rule)
let rules_for_files t paths =
List.filter_map paths ~f:(fun path ->
match Hashtbl.find t.files path with
| None -> None
| Some (File_spec.T { rule; _ }) -> Some rule)
|> Ir_set.of_list
|> Ir_set.elements
module Ir_closure =
Top_closure.Make(Internal_rule.Id)
(struct
type graph = t
type t = Internal_rule.t
let key (t : t) = t.id
let deps (t : t) bs =
rules_for_files bs
(Pset.elements
(Pset.union
t.static_deps
t.rule_deps))
end)
let rules_for_targets t targets =
match Ir_closure.top_closure t (rules_for_files t targets) with
| Ok l -> l
| Error cycle ->
die "dependency cycle detected:\n %s"
(List.map cycle ~f:(fun rule ->
Path.to_string (Pset.choose rule.Internal_rule.targets))
|> String.concat ~sep:"\n-> ")
let all_lib_deps t targets =
List.fold_left (rules_for_targets t targets) ~init:Pmap.empty
~f:(fun acc rule ->
let lib_deps = Build_interpret.lib_deps rule.Internal_rule.build in
Pmap.merge acc lib_deps ~f:(fun _ a b ->
match a, b with
| None, None -> None
| Some a, None -> Some a
| None, Some b -> Some b
| Some a, Some b -> Some (Build.merge_lib_deps a b)))
let all_lib_deps_by_context t targets =
List.fold_left (rules_for_targets t targets) ~init:[] ~f:(fun acc rule ->
let lib_deps = Build_interpret.lib_deps rule.Internal_rule.build in
Path.Map.fold lib_deps ~init:acc ~f:(fun ~key:path ~data:lib_deps acc ->
match Path.extract_build_context path with
| None -> acc
| Some (context, _) -> (context, lib_deps) :: acc))
|> String_map.of_alist_multi
|> String_map.map ~f:(function
| [] -> String_map.empty
| x :: l -> List.fold_left l ~init:x ~f:Build.merge_lib_deps)
module Rule = struct
module Id = Internal_rule.Id
type t =
{ id : Id.t
; deps : Path.Set.t
; targets : Path.Set.t
; context : Context.t option
; action : Action.t
}
let compare a b = Id.compare a.id b.id
end
module Rule_set = Set.Make(Rule)
module Id_set = Set.Make(Rule.Id)
let rules_for_files rules paths =
List.fold_left paths ~init:Rule_set.empty ~f:(fun acc path ->
match Pmap.find path rules with
| None -> acc
| Some rule -> Rule_set.add rule acc)
|> Rule_set.elements
module Rule_closure =
Top_closure.Make(Rule.Id)
(struct
type graph = Rule.t Pmap.t
type t = Rule.t
let key (t : t) = t.id
let deps (t : t) (graph : graph) =
rules_for_files graph (Pset.elements t.deps)
end)
let build_rules t ?(recursive=false) targets =
let rules_seen = ref Id_set.empty in
let rules = ref [] in
let rec loop fn =
match Hashtbl.find t.files fn with
| None -> return ()
| Some (File_spec.T { rule = ir; _ }) ->
if Id_set.mem ir.id !rules_seen then
return ()
else begin
rules_seen := Id_set.add ir.id !rules_seen;
let rule =
let make_rule rule_evaluation =
rule_evaluation >>| fun (action, dyn_deps) ->
{ Rule.
id = ir.id
; deps = Pset.union ir.static_deps dyn_deps
; targets = ir.targets
; context = ir.context
; action = action
}
in
match ir.exec with
| Starting _ -> assert false (* guarded by [rules_seen] *)
| Running { rule_evaluation; _ } | Evaluating_rule { rule_evaluation; _ } ->
make_rule rule_evaluation
| Not_started { eval_rule; exec_rule } ->
ir.exec <- Starting { for_file = fn };
let rule_evaluation =
wrap_build_errors t ~targeting:fn ~f:eval_rule
in
ir.exec <-
Evaluating_rule { for_file = fn
; rule_evaluation
; exec_rule
};
make_rule rule_evaluation
in
rules := rule :: !rules;
rule >>= fun rule ->
if recursive then
Future.all_unit (List.map (Pset.elements rule.deps) ~f:loop)
else
return ()
end
in
Future.all_unit (List.map targets ~f:loop)
>>= fun () ->
Future.all !rules
>>| fun rules ->
let rules =
List.fold_left rules ~init:Pmap.empty ~f:(fun acc (r : Rule.t) ->
Pset.fold r.targets ~init:acc ~f:(fun fn acc ->
Pmap.add acc ~key:fn ~data:r))
in
match Rule_closure.top_closure rules (rules_for_files rules targets) with
| Ok l -> l
| Error cycle ->
die "dependency cycle detected:\n %s"
(List.map cycle ~f:(fun rule -> Path.to_string (Pset.choose rule.Rule.targets))
|> String.concat ~sep:"\n-> ")