-
Notifications
You must be signed in to change notification settings - Fork 428
/
Copy pathbuild_system.ml
2420 lines (2183 loc) · 82.5 KB
/
build_system.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
open! Stdune
open Import
open Memo.Build.O
let () = Hooks.End_of_build.always Memo.reset
module Fs : sig
val mkdir_p : Path.Build.t -> unit Memo.Build.t
(** Creates directory if inside build path, otherwise asserts that directory
exists. *)
val mkdir_p_or_check_exists : loc:Loc.t -> Path.t -> unit Memo.Build.t
val assert_exists : loc:Loc.t -> Path.t -> unit Memo.Build.t
end = struct
let mkdir_p_def =
Memo.create "mkdir_p" ~doc:"mkdir_p"
~input:(module Path.Build)
~output:(Simple (module Unit))
~visibility:Hidden
(fun p ->
Path.mkdir_p (Path.build p);
Memo.Build.return ())
let mkdir_p = Memo.exec mkdir_p_def
let assert_exists_def =
Memo.create "assert_path_exists" ~doc:"Path.exists"
~input:(module Path)
~output:(Simple (module Bool))
~visibility:Hidden
(fun p -> Memo.Build.return (Path.exists p))
let assert_exists ~loc path =
Memo.exec assert_exists_def path >>| function
| false ->
User_error.raise ~loc
[ Pp.textf "%S does not exist" (Path.to_string_maybe_quoted path) ]
| true -> ()
let mkdir_p_or_check_exists ~loc path =
match Path.as_in_build_dir path with
| None -> assert_exists ~loc path
| Some path -> mkdir_p path
end
(* [Promoted_to_delete] is used mostly to implement [dune clean]. It is an
imperfect heuristic, in particular it can go wrong if:
- the user deletes .to-delete-in-source-tree file
- the user edits a previously promoted file with the intention of keeping it
in the source tree, or creates a new file with the same name *)
module Promoted_to_delete : sig
val add : Path.t -> unit
val remove : Path.t -> unit
val mem : Path.t -> bool
val get_db : unit -> Path.Set.t
end = struct
module P = Dune_util.Persistent.Make (struct
type t = Path.Set.t
let name = "PROMOTED-TO-DELETE"
let version = 1
let to_dyn = Path.Set.to_dyn
end)
let fn = Path.relative Path.build_dir ".to-delete-in-source-tree"
(* [db] is used to accumulate promoted files from rules. *)
let db = lazy (ref (Option.value ~default:Path.Set.empty (P.load fn)))
let get_db () = !(Lazy.force db)
let set_db new_db = Lazy.force db := new_db
let needs_dumping = ref false
let modify_db f =
match f (get_db ()) with
| None -> ()
| Some new_db ->
set_db new_db;
needs_dumping := true
let add p =
modify_db (fun db ->
if Path.Set.mem db p then
None
else
Some (Path.Set.add db p))
let remove p =
modify_db (fun db ->
if Path.Set.mem db p then
Some (Path.Set.remove db p)
else
None)
let dump () =
if !needs_dumping && Path.build_dir_exists () then (
needs_dumping := false;
get_db () |> P.dump fn
)
let mem p = Path.Set.mem !(Lazy.force db) p
let () = Hooks.End_of_build.always dump
end
let files_in_source_tree_to_delete () = Promoted_to_delete.get_db ()
let alias_exists_fdecl = Fdecl.create (fun _ -> Dyn.Opaque)
module Alias0 = struct
include Alias
let dep t = Action_builder.dep (Dep.alias t)
let dep_multi_contexts ~dir ~name ~contexts =
ignore (Source_tree.find_dir_specified_on_command_line ~dir);
let context_to_alias_expansion ctx =
let ctx_dir = Context_name.build_dir ctx in
let dir = Path.Build.(append_source ctx_dir dir) in
dep (make ~dir name)
in
Action_builder.all_unit (List.map contexts ~f:context_to_alias_expansion)
open Action_builder.O
module Source_tree_map_reduce =
Source_tree.Dir.Make_map_reduce (Action_builder) (Monoid.Exists)
let dep_rec_internal ~name ~dir ~ctx_dir =
let f dir =
let path = Path.Build.append_source ctx_dir (Source_tree.Dir.path dir) in
Action_builder.dep_on_alias_if_exists (make ~dir:path name)
in
Source_tree_map_reduce.map_reduce dir
~traverse:Sub_dirs.Status.Set.normal_only ~f
let dep_rec t ~loc =
let ctx_dir, src_dir =
Path.Build.extract_build_context_dir_exn (Alias.dir t)
in
Action_builder.memo_build (Source_tree.find_dir src_dir) >>= function
| None ->
Action_builder.fail
{ fail =
(fun () ->
User_error.raise ~loc
[ Pp.textf "Don't know about directory %s!"
(Path.Source.to_string_maybe_quoted src_dir)
])
}
| Some dir ->
let name = Alias.name t in
let+ is_nonempty = dep_rec_internal ~name ~dir ~ctx_dir in
if (not is_nonempty) && not (is_standard name) then
User_error.raise ~loc
[ Pp.text "This alias is empty."
; Pp.textf "Alias %S is not defined in %s or any of its descendants."
(Alias.Name.to_string name)
(Path.Source.to_string_maybe_quoted src_dir)
]
let dep_rec_multi_contexts ~dir:src_dir ~name ~contexts =
let open Action_builder.O in
let* dir =
Action_builder.memo_build
(Source_tree.find_dir_specified_on_command_line ~dir:src_dir)
in
let+ is_nonempty_list =
Action_builder.all
(List.map contexts ~f:(fun ctx ->
let ctx_dir = Context_name.build_dir ctx in
dep_rec_internal ~name ~dir ~ctx_dir))
in
let is_nonempty = List.exists is_nonempty_list ~f:Fun.id in
if (not is_nonempty) && not (is_standard name) then
User_error.raise
[ Pp.textf "Alias %S specified on the command line is empty."
(Alias.Name.to_string name)
; Pp.textf "It is not defined in %s or any of its descendants."
(Path.Source.to_string_maybe_quoted src_dir)
]
let package_install ~(context : Build_context.t) ~(pkg : Package.t) =
let dir =
let dir = Package.dir pkg in
Path.Build.append_source context.build_dir dir
in
let name = Package.name pkg in
sprintf ".%s-files" (Package.Name.to_string name)
|> Alias.Name.of_string |> make ~dir
end
module Loaded = struct
type build =
{ allowed_subdirs : Path.Unspecified.w Dir_set.t
; rules_produced : Rules.t
; rules_here : Rule.t Path.Build.Map.t
; aliases : (Loc.t * unit Action_builder.t) list Alias.Name.Map.t
}
type t =
| Non_build of Path.Set.t
| Build of build
let no_rules ~allowed_subdirs =
Build
{ allowed_subdirs
; rules_produced = Rules.empty
; rules_here = Path.Build.Map.empty
; aliases = Alias.Name.Map.empty
}
end
module Dir_triage = struct
type t =
| Known of Loaded.t
| Need_step2
end
(* Stores information needed to determine if rule need to be reexecuted. *)
module Trace_db : sig
module Entry : sig
type t =
{ rule_digest : Digest.t
; dynamic_deps_stages : (Action_exec.Dynamic_dep.Set.t * Digest.t) list
; targets_digest : Digest.t
}
end
val get : Path.t -> Entry.t option
val set : Path.t -> Entry.t -> unit
end = struct
module Entry = struct
type t =
{ rule_digest : Digest.t
; dynamic_deps_stages : (Action_exec.Dynamic_dep.Set.t * Digest.t) list
; targets_digest : Digest.t
}
let to_dyn { rule_digest; dynamic_deps_stages; targets_digest } =
Dyn.Record
[ ("rule_digest", Digest.to_dyn rule_digest)
; ( "dynamic_deps_stages"
, Dyn.Encoder.list
(Dyn.Encoder.pair Action_exec.Dynamic_dep.Set.to_dyn Digest.to_dyn)
dynamic_deps_stages )
; ("targets_digest", Digest.to_dyn targets_digest)
]
end
(* Keyed by the first target of the rule. *)
type t = Entry.t Path.Table.t
let file = Path.relative Path.build_dir ".db"
let to_dyn = Path.Table.to_dyn Entry.to_dyn
module P = Dune_util.Persistent.Make (struct
type nonrec t = t
let name = "INCREMENTAL-DB"
let version = 4
let to_dyn = to_dyn
end)
let needs_dumping = ref false
let t =
(* This [lazy] is safe: it does not call any memoized functions. *)
lazy
(match P.load file with
| Some t -> t
(* This mutable table is safe: it's only used by [execute_rule_impl] to
decide whether to rebuild a rule or not; [execute_rule_impl] ensures
that the targets are produced deterministically. *)
| None -> Path.Table.create 1024)
let dump () =
if !needs_dumping && Path.build_dir_exists () then (
needs_dumping := false;
P.dump file (Lazy.force t)
)
let () = Hooks.End_of_build.always dump
let get path =
let t = Lazy.force t in
Path.Table.find t path
let set path e =
let t = Lazy.force t in
needs_dumping := true;
Path.Table.set t path e
end
module Subdir_set = struct
type t =
| All
| These of String.Set.t
let to_dir_set = function
| All -> Dir_set.universal
| These s ->
String.Set.fold s ~init:Dir_set.empty ~f:(fun path acc ->
let path = Path.Local.of_string path in
Dir_set.union acc (Dir_set.singleton path))
let of_dir_set d =
match Dir_set.toplevel_subdirs d with
| Infinite -> All
| Finite s -> These s
let of_list l = These (String.Set.of_list l)
let empty = These String.Set.empty
let mem t dir =
match t with
| All -> true
| These t -> String.Set.mem t dir
let union a b =
match (a, b) with
| All, _
| _, All ->
All
| These a, These b -> These (String.Set.union a b)
let union_all = List.fold_left ~init:empty ~f:union
end
type extra_sub_directories_to_keep = Subdir_set.t
module Rule_fn = struct
let loc_decl = Fdecl.create Dyn.Encoder.opaque
let loc () = Fdecl.get loc_decl ()
end
module Context_or_install = struct
type t =
| Install of Context_name.t
| Context of Context_name.t
let to_dyn = function
| Install ctx -> Dyn.List [ Dyn.String "install"; Context_name.to_dyn ctx ]
| Context s -> Context_name.to_dyn s
end
type caching =
{ cache : (module Cache.Caching)
; check_probability : float
}
type t =
{ contexts : Build_context.t Context_name.Map.t
; init_rules : Rules.t Fdecl.t
; gen_rules :
( Context_or_install.t
-> ( dir:Path.Build.t
-> string list
-> extra_sub_directories_to_keep Memo.Build.t)
option)
Fdecl.t
; (* Package files are part of *)
packages : (Path.Build.t -> Package.Id.Set.t Memo.Build.t) Fdecl.t
; mutable caching : caching option
; sandboxing_preference : Sandbox_mode.t list
; mutable rule_done : int
; mutable rule_total : int
; mutable errors : Exn_with_backtrace.t list
; vcs : Vcs.t list Fdecl.t
; promote_source :
?chmod:(int -> int)
-> src:Path.Build.t
-> dst:Path.Source.t
-> Build_context.t option
-> unit Fiber.t
; locks : (Path.t, Fiber.Mutex.t) Table.t
; build_mutex : Fiber.Mutex.t option
; stats : Stats.t option
}
let t = ref None
let set x =
match !t with
| None -> t := Some x
| Some _ -> Code_error.raise "build system already initialized" []
let get_build_system () =
match !t with
| Some t -> t
| None -> Code_error.raise "build system not yet initialized" []
let reset () = t := None
let t = get_build_system
let contexts () = (t ()).contexts
let pp_paths set =
Pp.enumerate (Path.Set.to_list set) ~f:(fun p ->
Path.drop_optional_build_context p
|> Path.to_string_maybe_quoted |> Pp.verbatim)
let set_rule_generators ~init ~gen_rules =
let t = t () in
let open Fiber.O in
let+ init_rules = Memo.Build.run (Rules.collect_unit init) in
Fdecl.set t.init_rules init_rules;
Fdecl.set t.gen_rules gen_rules
let set_vcs vcs =
let open Fiber.O in
let t = t () in
let () = Fdecl.set t.vcs vcs in
match t.caching with
| None -> Fiber.return ()
| Some ({ cache = (module Caching); _ } as caching) ->
let+ caching =
let+ with_repositories =
let f ({ Vcs.root; _ } as vcs) =
let+ commit = Memo.Build.run (Vcs.commit_id vcs) in
{ Cache.directory = Path.to_absolute_filename root
; remote = "" (* FIXME: fill or drop from the protocol *)
; commit
}
in
let+ repositories = Fiber.parallel_map ~f (Fdecl.get t.vcs) in
Caching.Cache.with_repositories Caching.cache repositories
in
match with_repositories with
| Result.Ok cache ->
let cache =
(module struct
let cache = cache
module Cache = Caching.Cache
end : Cache.Caching)
in
Some { caching with cache }
| Result.Error e ->
User_warning.emit
[ Pp.textf "Unable to set cache repositiories, disabling cache: %s" e
];
None
in
t.caching <- caching
let get_vcs () =
let t = t () in
Fdecl.get t.vcs
let get_cache () =
let t = t () in
t.caching
let get_dir_triage t ~dir =
match Dpath.analyse_dir dir with
| Source dir ->
let+ files = Source_tree.files_of dir in
Dir_triage.Known (Non_build (Path.set_of_source_paths files))
| External _ ->
Memo.Build.return
@@ Dir_triage.Known
(Non_build
(match Path.readdir_unsorted dir with
| Error Unix.ENOENT -> Path.Set.empty
| Error m ->
User_warning.emit
[ Pp.textf "Unable to read %s" (Path.to_string_maybe_quoted dir)
; Pp.textf "Reason: %s" (Unix.error_message m)
];
Path.Set.empty
| Ok filenames -> Path.Set.of_listing ~dir ~filenames))
| Build (Regular Root) ->
let allowed_subdirs =
Subdir_set.to_dir_set
(Subdir_set.of_list
(([ Dpath.Build.anonymous_actions_dir; Dpath.Build.install_dir ]
|> List.map ~f:Path.Build.basename)
@ (Context_name.Map.keys t.contexts
|> List.map ~f:Context_name.to_string)))
in
Memo.Build.return @@ Dir_triage.Known (Loaded.no_rules ~allowed_subdirs)
| Build (Install Root) ->
let allowed_subdirs =
Context_name.Map.keys t.contexts
|> List.map ~f:Context_name.to_string
|> Subdir_set.of_list |> Subdir_set.to_dir_set
in
Memo.Build.return @@ Dir_triage.Known (Loaded.no_rules ~allowed_subdirs)
| Build (Anonymous_action p) ->
let build_dir = Dpath.Target_dir.build_dir p in
Code_error.raise "Called get_dir_triage on an anonymous action directory"
[ ("dir", Path.Build.to_dyn build_dir) ]
| Build (Invalid _) ->
Memo.Build.return
@@ Dir_triage.Known (Loaded.no_rules ~allowed_subdirs:Dir_set.empty)
| Build (Install (With_context _))
| Build (Regular (With_context _)) ->
Memo.Build.return @@ Dir_triage.Need_step2
let describe_rule (rule : Rule.t) =
match rule.info with
| From_dune_file { start; _ } ->
start.pos_fname ^ ":" ^ string_of_int start.pos_lnum
| Internal -> "<internal location>"
| Source_file_copy _ -> "file present in source tree"
let report_rule_src_dir_conflict dir fn (rule : Rule.t) =
let loc =
match rule.info with
| From_dune_file loc -> loc
| Internal
| Source_file_copy _ ->
let dir =
match Path.Build.drop_build_context dir with
| None -> Path.build dir
| Some s -> Path.source s
in
Loc.in_dir dir
in
User_error.raise ~loc
[ Pp.textf "Rule has a target %s" (Path.Build.to_string_maybe_quoted fn)
; Pp.textf "This conflicts with a source directory in the same directory"
]
let report_rule_conflict fn (rule' : Rule.t) (rule : Rule.t) =
let fn = Path.build fn in
User_error.raise
[ Pp.textf "Multiple rules generated for %s:"
(Path.to_string_maybe_quoted fn)
; Pp.textf "- %s" (describe_rule rule')
; Pp.textf "- %s" (describe_rule rule)
]
~hints:
(match (rule.info, rule'.info) with
| Source_file_copy _, _
| _, Source_file_copy _ ->
[ Pp.textf "rm -f %s"
(Path.to_string_maybe_quoted (Path.drop_optional_build_context fn))
]
| _ -> [])
(* 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 Path.Build.Set.empty
let () =
Hooks.End_of_build.always (fun () ->
let fns = !pending_targets in
pending_targets := Path.Build.Set.empty;
Path.Build.Set.iter fns ~f:(fun p -> Path.unlink_no_err (Path.build p)))
let compute_targets_digests targets =
match
List.map (Path.Build.Set.to_list targets) ~f:(fun target ->
(target, Cached_digest.build_file target))
with
| l -> Some l
| exception (Unix.Unix_error _ | Sys_error _) -> None
let compute_targets_digests_or_raise_error ~loc targets =
let open Fiber.O in
let+ remove_write_permissions =
(* Remove write permissions on targets. A first theoretical reason is that
the build process should be a computational graph and targets should not
change state once built. A very practical reason is that enabling the
cache will remove write permission because of hardlink sharing anyway, so
always removing them enables to catch mistakes earlier. *)
(* FIXME: searching the dune version for each single target seems way
suboptimal. This information could probably be stored in rules directly. *)
if Path.Build.Set.is_empty targets then
Fiber.return false
else
let _, src_dir =
Path.Build.extract_build_context_dir_exn
(Path.Build.Set.choose_exn targets)
in
let+ dir = Memo.Build.run (Source_tree.nearest_dir src_dir) in
let version = Source_tree.Dir.project dir |> Dune_project.dune_version in
version >= (2, 4)
in
let refresh =
if remove_write_permissions then
Cached_digest.refresh_and_chmod
else
Cached_digest.refresh
in
let good, bad =
Path.Build.Set.fold targets ~init:([], []) ~f:(fun target (good, bad) ->
match refresh target with
| digest -> ((target, digest) :: good, bad)
| exception (Unix.Unix_error _ | Sys_error _) ->
(good, Path.build target :: bad))
in
match bad with
| [] -> List.rev good
| missing ->
User_error.raise ~loc
[ Pp.textf "Rule failed to generate the following targets:"
; pp_paths (Path.Set.of_list missing)
]
let sandbox_dir = Path.Build.relative Path.Build.root ".sandbox"
let rec with_locks t mutexes ~f =
match mutexes with
| [] -> f ()
| m :: mutexes ->
Fiber.Mutex.with_lock
(Table.find_or_add t.locks m ~f:(fun _ -> Fiber.Mutex.create ()))
(fun () -> with_locks t mutexes ~f)
let remove_old_artifacts ~dir ~rules_here ~(subdirs_to_keep : Subdir_set.t) =
match Path.readdir_unsorted_with_kinds (Path.build dir) with
| exception _ -> ()
| Error _ -> ()
| Ok files ->
List.iter files ~f:(fun (fn, kind) ->
let path = Path.Build.relative dir fn in
let path_is_a_target = Path.Build.Map.mem rules_here path in
if not path_is_a_target then
match kind with
| Unix.S_DIR -> (
match subdirs_to_keep with
| All -> ()
| These set ->
if not (String.Set.mem set fn) then Path.rm_rf (Path.build path))
| _ -> Path.unlink (Path.build path))
(* We don't remove files in there as we don't know upfront if they are stale or
not. *)
let remove_old_sub_dirs_in_anonymous_actions_dir ~dir
~(subdirs_to_keep : Subdir_set.t) =
match Path.readdir_unsorted_with_kinds (Path.build dir) with
| exception _ -> ()
| Error _ -> ()
| Ok files ->
List.iter files ~f:(fun (fn, kind) ->
let path = Path.Build.relative dir fn in
match kind with
| Unix.S_DIR -> (
match subdirs_to_keep with
| All -> ()
| These set ->
if not (String.Set.mem set fn) then Path.rm_rf (Path.build path))
| _ -> ())
let no_rule_found t ~loc fn =
let fail fn ~loc =
User_error.raise ?loc
[ Pp.textf "No rule found for %s" (Dpath.describe_target fn) ]
in
let hints ctx =
let candidates =
Context_name.Map.keys t.contexts |> List.map ~f:Context_name.to_string
in
User_message.did_you_mean (Context_name.to_string ctx) ~candidates
in
match Dpath.analyse_target fn with
| Other _ -> fail fn ~loc
| Regular (ctx, _) ->
if Context_name.Map.mem t.contexts ctx then
fail fn ~loc
else
User_error.raise
[ Pp.textf "Trying to build %s but build context %s doesn't exist."
(Path.Build.to_string_maybe_quoted fn)
(Context_name.to_string ctx)
]
~hints:(hints ctx)
| Install (ctx, _) ->
if Context_name.Map.mem t.contexts ctx then
fail fn ~loc
else
User_error.raise
[ Pp.textf
"Trying to build %s for install but build context %s doesn't exist."
(Path.Build.to_string_maybe_quoted fn)
(Context_name.to_string ctx)
]
~hints:(hints ctx)
| Alias (ctx, fn') ->
if Context_name.Map.mem t.contexts ctx then
fail fn ~loc
else
let fn =
Path.append_source (Path.build (Context_name.build_dir ctx)) fn'
in
User_error.raise
[ Pp.textf
"Trying to build alias %s but build context %s doesn't exist."
(Path.to_string_maybe_quoted fn)
(Context_name.to_string ctx)
]
~hints:(hints ctx)
| Anonymous_action _ ->
(* We never lookup such actions by target name, so this should be
unreachable *)
Code_error.raise ?loc "Build_system.no_rule_found got anonymous action path"
[ ("fn", Path.Build.to_dyn fn) ]
(* +-------------------- Adding rules to the system --------------------+ *)
module rec Load_rules : sig
val load_dir : dir:Path.t -> Loaded.t Memo.Build.t
val file_exists : Path.t -> bool Memo.Build.t
val targets_of : dir:Path.t -> Path.Set.t Memo.Build.t
val lookup_alias :
Alias.t -> (Loc.t * unit Action_builder.t) list option Memo.Build.t
end = struct
open Load_rules
let create_copy_rules ~ctx_dir ~non_target_source_files =
Path.Source.Set.to_list_map non_target_source_files ~f:(fun path ->
let ctx_path = Path.Build.append_source ctx_dir path in
let build = Action_builder.copy ~src:(Path.source path) ~dst:ctx_path in
Rule.make
(* There's an [assert false] in [prepare_managed_paths] that blows up if
we try to sandbox this. *)
~sandbox:Sandbox_config.no_sandboxing build ~context:None ~env:None
~info:(Source_file_copy path))
let compile_rules ~dir ~source_dirs rules =
List.concat_map rules ~f:(fun rule ->
assert (Path.Build.( = ) dir rule.Rule.dir);
Path.Build.Set.to_list_map rule.action.targets ~f:(fun target ->
if String.Set.mem source_dirs (Path.Build.basename target) then
report_rule_src_dir_conflict dir target rule
else
(target, rule)))
|> Path.Build.Map.of_list_reducei ~f:report_rule_conflict
(* Here we are doing a O(log |S|) lookup in a set S of files in the build
directory [dir]. We could memoize these lookups, but it doesn't seem to be
worth it, since we're unlikely to perform exactly the same lookup many
times. As far as I can tell, each lookup will be done twice: when computing
static dependencies of a [Action_builder.t] with
[Action_builder.static_deps] and when executing the very same
[Action_builder.t] with [Action_builder.exec] -- the results of both
[Action_builder.static_deps] and [Action_builder.exec] are cached. *)
let file_exists fn =
load_dir ~dir:(Path.parent_exn fn) >>| function
| Non_build targets -> Path.Set.mem targets fn
| Build { rules_here; _ } -> (
match Path.as_in_build_dir fn with
| None -> false
| Some fn -> Path.Build.Map.mem rules_here fn)
let targets_of ~dir =
load_dir ~dir >>| function
| Non_build targets -> targets
| Build { rules_here; _ } ->
Path.Build.Map.keys rules_here |> Path.Set.of_list_map ~f:Path.build
let lookup_alias alias =
load_dir ~dir:(Path.build (Alias.dir alias)) >>| function
| Non_build _ ->
Code_error.raise "Alias in a non-build dir"
[ ("alias", Alias.to_dyn alias) ]
| Build { aliases; _ } -> Alias.Name.Map.find aliases (Alias.name alias)
let () =
Fdecl.set alias_exists_fdecl (fun alias ->
lookup_alias alias >>| function
| None -> false
| Some _ -> true)
let compute_alias_expansions ~(collected : Rules.Dir_rules.ready) ~dir =
let aliases = collected.aliases in
let+ aliases =
if Alias.Name.Map.mem aliases Alias.Name.default then
Memo.Build.return aliases
else
match Path.Build.extract_build_context_dir dir with
| None -> Memo.Build.return aliases
| Some (ctx_dir, src_dir) -> (
Source_tree.find_dir src_dir >>| function
| None -> aliases
| Some dir ->
let default_alias =
let dune_version =
Source_tree.Dir.project dir |> Dune_project.dune_version
in
if dune_version >= (2, 0) then
Alias.Name.all
else
Alias.Name.install
in
Alias.Name.Map.set aliases Alias.Name.default
{ expansions =
Appendable_list.singleton
( Loc.none
, let open Action_builder.O in
let+ _ =
Alias0.dep_rec_internal ~name:default_alias ~dir
~ctx_dir
in
() )
})
in
Alias.Name.Map.map aliases
~f:(fun { Rules.Dir_rules.Alias_spec.expansions } ->
Appendable_list.to_list expansions)
let filter_out_fallback_rules ~to_copy rules =
List.filter rules ~f:(fun (rule : Rule.t) ->
match rule.mode with
| Standard
| Promote _
| Ignore_source_files ->
true
| Fallback ->
let source_files_for_targtes =
(* All targets are in [dir] and we know it correspond to a directory
of a build context since there are source files to copy, so this
call can't fail. *)
Path.Build.Set.to_list rule.action.targets
|> Path.Source.Set.of_list_map ~f:Path.Build.drop_build_context_exn
in
if Path.Source.Set.is_subset source_files_for_targtes ~of_:to_copy
then
(* All targets are present *)
false
else if
Path.Source.Set.is_empty
(Path.Source.Set.inter source_files_for_targtes to_copy)
then
(* No target is present *)
true
else
let absent_targets =
Path.Source.Set.diff source_files_for_targtes to_copy
in
let present_targets =
Path.Source.Set.diff source_files_for_targtes absent_targets
in
User_error.raise ~loc:(Rule.loc rule)
[ Pp.text
"Some of the targets of this fallback rule are present in \
the source tree, and some are not. This is not allowed. \
Either none of the targets must be present in the source \
tree, either they must all be."
; Pp.nop
; Pp.text "The following targets are present:"
; pp_paths (Path.set_of_source_paths present_targets)
; Pp.nop
; Pp.text "The following targets are not:"
; pp_paths (Path.set_of_source_paths absent_targets)
])
(** A directory is only allowed to be generated if its parent knows about it.
This restriction is necessary to prevent stale artifact deletion from
removing that directory.
This module encodes that restriction. *)
module Generated_directory_restrictions : sig
type restriction =
| Unrestricted
| Restricted of Path.Unspecified.w Dir_set.t Memo.Lazy.t
(** Used by the child to ask about the restrictions placed by the parent. *)
val allowed_by_parent : dir:Path.Build.t -> restriction Memo.Build.t
end = struct
type restriction =
| Unrestricted
| Restricted of Path.Unspecified.w Dir_set.t Memo.Lazy.t
let corresponding_source_dir ~dir =
match Dpath.analyse_target dir with
| Install _
| Alias _
| Anonymous_action _
| Other _ ->
Memo.Build.return None
| Regular (_ctx, sub_dir) -> Source_tree.find_dir sub_dir
let source_subdirs_of_build_dir ~dir =
corresponding_source_dir ~dir >>| function
| None -> String.Set.empty
| Some dir -> Source_tree.Dir.sub_dir_names dir
let allowed_dirs ~dir ~subdir : restriction Memo.Build.t =
let+ subdirs = source_subdirs_of_build_dir ~dir in
if String.Set.mem subdirs subdir then
Unrestricted
else
Restricted
(Memo.Lazy.create (fun () ->
load_dir ~dir:(Path.build dir) >>| function
| Non_build _ -> Dir_set.just_the_root
| Build { allowed_subdirs; _ } ->
Dir_set.descend allowed_subdirs subdir))
let allowed_by_parent ~dir =
allowed_dirs
~dir:(Path.Build.parent_exn dir)
~subdir:(Path.Build.basename dir)
end
(* TODO: Delete this step after users of dune <2.8 are sufficiently rare. This
step is sketchy because it's using the [Promoted_to_delete] database and
that can get out of date (see a comment on [Promoted_to_delete]), so we
should not widen the scope of it too much. *)
let delete_stale_dot_merlin_file ~dir ~source_files_to_ignore =
(* If a [.merlin] file is present in the [Promoted_to_delete] set but not in
the [Source_files_to_ignore] that means the rule that ordered its
promotion is no more valid. This would happen when upgrading to Dune 2.8
from ealier version without and building uncleaned projects. We delete
these leftover files here. *)
let merlin_file = ".merlin" in
let source_dir = Path.Build.drop_build_context_exn dir in
let merlin_in_src = Path.Source.(relative source_dir merlin_file) in
let source_files_to_ignore =
if
Promoted_to_delete.mem (Path.source merlin_in_src)
&& not (Path.Source.Set.mem source_files_to_ignore merlin_in_src)
then (
let path = Path.source merlin_in_src in
Log.info
[ Pp.textf "Deleting left-over Merlin file %s.\n"
(Path.to_string path)
];
(* We remove the file from the promoted database *)
Promoted_to_delete.remove path;
Path.unlink_no_err path;
(* We need to keep ignoring the .merlin file for that build or Dune will
attempt to copy it and fail because it has been deleted *)
Path.Source.Set.add source_files_to_ignore merlin_in_src
) else
source_files_to_ignore
in
source_files_to_ignore
let load_dir_step2_exn t ~dir =
let context_name, sub_dir =
match Dpath.analyse_path dir with
| Build (Install (ctx, path)) -> (Context_or_install.Install ctx, path)
| Build (Regular (ctx, path)) -> (Context_or_install.Context ctx, path)
| Build (Alias _)
| Build (Anonymous_action _)
| Build (Other _)
| Source _
| External _ ->
Code_error.raise "[load_dir_step2_exn] was called on a strange path"
[ ("path", Path.to_dyn dir) ]
in
(* the above check makes this safe *)
let dir = Path.as_in_build_dir_exn dir in
(* Load all the rules *)
let* extra_subdirs_to_keep, rules_produced =
let gen_rules =
match (Fdecl.get t.gen_rules) context_name with
| None ->
Code_error.raise "[gen_rules] did not specify rules for the context"
[ ("context_name", Context_or_install.to_dyn context_name) ]
| Some f -> f
in
Rules.collect (fun () -> gen_rules ~dir (Path.Source.explode sub_dir))
in
let rules =
let dir = Path.build dir in
Rules.Dir_rules.union
(Rules.find rules_produced dir)
(Rules.find (Fdecl.get t.init_rules) dir)
in
let collected = Rules.Dir_rules.consume rules in
let rules = collected.rules in
let* aliases =
match context_name with
| Context _ -> compute_alias_expansions ~collected ~dir
| Install _ ->
(* There are no aliases in the [_build/install] directory *)
Memo.Build.return Alias.Name.Map.empty
and* source_tree_dir =
match context_name with
| Install _ -> Memo.Build.return None
| Context _ -> Source_tree.find_dir sub_dir
in
(* Compute the set of targets and the set of source files that must not be
copied *)
let source_files_to_ignore =