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
open! Core
open! Async
open! Import
let tar_id = "dynlink.tgz"
;;
let ocamlopt_opt = "ocamlopt.opt"
let ocamldep_opt = "ocamldep.opt"
let ppx_exe = "ppx.exe"
;;
let persistent_archive_subdir = "compiler"
;;
module Archive_metadata = struct
type t =
{ ppx_is_embedded : bool
; archive_digests : Plugin_cache.Digest.t String.Map.t
}
[@@deriving sexp]
end
module Archive_lock = struct
type t =
| Cleaned
| Cleaning of unit Deferred.Or_error.t
| Locked of string
end
type t =
{ loader : Dynloader.t
; archive_lock : Archive_lock.t ref
}
[@@deriving fields]
let clean t =
Dynloader.clean t.loader >>= fun r1 ->
(match t.archive_lock.contents with
| Archive_lock.Cleaned -> Deferred.Or_error.ok_unit
| Archive_lock.Cleaning def -> def
| Archive_lock.Locked lock_filename ->
let clean = Lock_file_async.Nfs.unlock lock_filename in
t.archive_lock.contents <- Archive_lock.Cleaning clean;
clean >>| fun res ->
t.archive_lock.contents <- Archive_lock.Cleaned;
res)
>>| fun r2 ->
Or_error.combine_errors_unit
[ r1
; r2
]
;;
external archive : unit -> Bigstring.t = "ocaml_plugin_archive"
let archive () =
let bstr = archive () in
let dummy = "dummy" in
if Bigstring.length bstr = String.length dummy &&
String.equal (Bigstring.to_string bstr) dummy then
None
else
Some bstr
;;
external archive_metadata_binding : unit -> string = "ocaml_plugin_archive_metadata"
let archive_metadata =
lazy (
let str = archive_metadata_binding () in
let dummy = "dummy" in
if String.equal str dummy
then Or_error.error_string "\
This executable does not have an embedded archive, although this is required when using \
[Ocaml_plugin]. A likely cause is that the build of the binary is missing the step \
involving [ocaml_embed_compiler]."
else Ok (Sexp.of_string_conv_exn str [%of_sexp: Archive_metadata.t]))
;;
let embedded_files () =
Or_error.map (force archive_metadata) ~f:(fun m -> m.archive_digests)
;;
let () =
match Core.Sys.getenv "OCAML_PLUGIN_DUMP_ARCHIVE" with
| None -> ()
| Some _ ->
(match force archive_metadata with
| Error _ -> Core.Printf.eprintf "No archive metadata\n%!"
| Ok archive_metadata ->
Core.Printf.eprintf !"archive metadata: %{sexp:Archive_metadata.t}\n%!"
archive_metadata);
(match archive () with
| None -> Core.Printf.printf "No archive\n%!"
| Some bstr -> Bigstring_unix.really_output stdout bstr; Out_channel.flush stdout);
Core.Caml.exit 0
;;
let save_archive_to destination =
Deferred.Or_error.try_with (fun () ->
match archive () with
| None -> failwith "There is no embedded compiler in the current executable"
| Some contents -> Writer.with_file_atomic destination ~f:(fun w ->
Writer.schedule_bigstring w contents;
Deferred.unit))
;;
type 'a create_arguments = (
?persistent_archive_dirpath:string
-> 'a
) Dynloader.create_arguments
module Plugin_archive : sig
end = struct
module Info = struct
type t =
{ infos : (string * Sexp.t) list
; build_info : Sexp.t
; archive_metadata : Archive_metadata.t
}
[@@deriving sexp]
let t_of_sexp = Sexp.of_sexp_allow_extra_fields_recursively t_of_sexp
let info_file_name = "archive-info.sexp"
let info_file dir = dir ^/ info_file_name
;;
let create () =
return (force archive_metadata) >>=? fun archive_metadata ->
Deferred.Or_error.try_with ~extract_exn:true (fun () ->
Unix.getlogin () >>| fun login ->
[ "version" , sexp_of_string Params.version
; "login" , sexp_of_string login
; "hostname" , sexp_of_string (Unix.gethostname ())
; "sys_argv" , [%sexp_of: string array] (Sys.get_argv ())
]
) >>|? fun infos ->
let build_info = Params.build_info_as_sexp in
{ infos
; build_info
; archive_metadata
}
;;
let info_file_perm = 0o644
;;
let save dir =
create () >>=? fun t ->
Deferred.Or_error.try_with ~extract_exn:true (fun () ->
Writer.save_sexp ~perm:info_file_perm (info_file dir) (sexp_of_t t)
)
;;
let load dir =
Deferred.Or_error.try_with ~extract_exn:true (fun () ->
Reader.load_sexp_exn (info_file dir) t_of_sexp
)
;;
let is_up_to_date t ~dir =
match force archive_metadata with
| Error _ as error -> return error
| Ok archive_metadata ->
let digests = archive_metadata.archive_digests in
if [%compare.equal: Plugin_cache.Digest.t String.Map.t]
digests t.archive_metadata.archive_digests
then (
Deferred.Or_error.try_with (fun () -> Sys.readdir dir)
>>|? fun files ->
let =
Set.diff
(String.Set.of_list (Array.to_list files))
(String.Set.of_list [ info_file_name; tar_id ])
in
let = Map.key_set digests in
String.Set.equal files_extracted files_we_would_extract)
else return (Ok false)
;;
end
let = Throttle.Sequencer.create ~continue_on_error:true ()
;;
let ~archive_lock ~persistent compiler_dir =
let () =
if_ persistent (fun () ->
let lock_filename = compiler_dir ^ ".lock" in
Monitor.try_with_or_error (fun () ->
Unix.mkdir ~p:() ~perm:0o755 (Filename.dirname lock_filename)) >>=? fun () ->
Lock_file_async.Nfs.create lock_filename >>=? fun () ->
archive_lock := Archive_lock.Locked lock_filename;
Shell.rm ~r:() ~f:() [ compiler_dir ] >>=? fun () ->
Monitor.try_with_or_error (fun () ->
Unix.mkdir ~p:() ~perm:0o755 compiler_dir)
) >>=? fun () ->
let destination = compiler_dir ^/ tar_id in
save_archive_to destination >>=? fun () ->
Tar.extract ~working_dir:compiler_dir destination >>=? fun () ->
if_ persistent (fun () -> Info.save compiler_dir)
in
if persistent
then Throttle.enqueue extract_throttle (fun () ->
Info.load compiler_dir >>= function
| Error _ -> extract ()
| Ok info ->
Info.is_up_to_date info ~dir:compiler_dir
>>= function
| Ok true -> Deferred.Or_error.ok_unit
| Error _ | Ok false -> extract ()
)
else
extract ()
;;
end
let create
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
?cmx_flags
?cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
?persistent_archive_dirpath
() =
let archive_lock = ref Archive_lock.Cleaned in
(match persistent_archive_dirpath with
| None -> Deferred.return (Ok None)
| Some path ->
Shell.absolute_pathname path >>|? fun path ->
Some (path ^/ persistent_archive_subdir)
) >>=? fun persistent_archive_dirpath ->
let in_compiler_dir exec =
Option.value persistent_archive_dirpath ~default:"." ^/ exec
in
let include_directories =
match persistent_archive_dirpath with
| None -> include_directories
| Some dir -> Some (dir :: Option.value include_directories ~default:[])
in
let ocamlopt_opt = in_compiler_dir ocamlopt_opt in
let ocamldep_opt = in_compiler_dir ocamldep_opt in
let nostdlib flags = "-nostdlib" :: Option.value ~default:[] flags in
let cmx_flags = nostdlib cmx_flags in
let cmxs_flags = nostdlib cmxs_flags in
let preprocessor =
match force archive_metadata with
| Error _ as error -> error
| Ok { ppx_is_embedded; archive_digests = _ } ->
if ppx_is_embedded
then Ok (Dynloader.Preprocessor.Ppx { ppx_exe = in_compiler_dir ppx_exe })
else Ok Dynloader.Preprocessor.No_preprocessing
in
return preprocessor
>>=? fun preprocessor ->
let compilation_config = { Dynloader.Compilation_config.preprocessor } in
let initialize ~directory:build_dir =
let persistent, compiler_dir =
match persistent_archive_dirpath with
| None -> false, build_dir
| Some archive_dirpath -> true, archive_dirpath
in
Plugin_archive.extract ~archive_lock ~persistent compiler_dir
in
Dynloader.create
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
~cmx_flags
~cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
~initialize
~compilation_config
~ocamlopt_opt
~ocamldep_opt
()
>>=? fun loader ->
let compiler =
{ loader
; archive_lock
}
in
Deferred.return (Ok (`this_needs_manual_cleaning_after compiler))
;;
let created_but_not_cleaned = Bag.create ()
;;
let () =
Shutdown.at_shutdown (fun () ->
Deferred.List.iter (Bag.to_list created_but_not_cleaned) ~f:(fun compiler ->
clean compiler >>| function
| Ok () -> ()
| Error _ -> ()))
;;
let is_shutting_down () =
match Shutdown.shutting_down () with
| `No -> false
| `Yes _ -> true
;;
let with_compiler
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
?cmx_flags
?cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
?persistent_archive_dirpath
~f
()
=
if is_shutting_down ()
then return (Or_error.error_s [%sexp "Shutting_down", [%here]])
else (
create
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
?cmx_flags
?cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
?persistent_archive_dirpath
()
>>=? fun (`this_needs_manual_cleaning_after compiler) ->
if is_shutting_down ()
then (
clean compiler >>=? fun () ->
return (Or_error.error_s [%sexp "Shutting_down", [%here]]))
else (
let bag_elem = Bag.add created_but_not_cleaned compiler in
Deferred.Or_error.try_with_join ~extract_exn:true (fun () -> f compiler)
>>= fun result ->
Bag.remove created_but_not_cleaned bag_elem;
clean compiler >>| fun r2 ->
match result, r2 with
| Ok result, Ok () -> Ok result
| Ok _, (Error _ as error) -> error
| Error e1, Error e2 -> Error (Error.of_list [e1; e2])
| Error _ as error, Ok () -> error))
;;
let make_load_ocaml_src_files load_ocaml_src_files =
let aux
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
?cmx_flags
?cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
?persistent_archive_dirpath
files =
let f compiler =
let loader = loader compiler in
load_ocaml_src_files loader files
in
with_compiler
?in_dir
?in_dir_perm
?include_directories
?custom_warnings_spec
?strict_sequence
?cmx_flags
?cmxs_flags
?trigger_unused_value_warnings_despite_mli
?use_cache
?persistent_archive_dirpath
~f
()
in
aux
;;
let make_check_plugin_cmd
~check_ocaml_src_files
~load_ocaml_src_files () =
let execute_plugin_toplevel_switch = "-execute-plugin-toplevel" in
Command.async ~summary:"Check a plugin for compilation errors"
~readme:(fun () -> String.concat [ "\
This command checks that a plugin compiles. It either succeeds quietly, or outputs
compilation errors and fails.
When it is deemed safe to execute the toplevel of a plugin, one can supply the switch
[" ; execute_plugin_toplevel_switch ; "] to check for runtime exceptions at toplevel." ])
(let open Command.Let_syntax in
let%map_open plugin_filenames = anon (sequence ("path/to/plugin.ml" %: Filename.arg_type))
and execute_plugin_toplevel =
flag execute_plugin_toplevel_switch no_arg
~doc:" Run the plugin's toplevel to check for runtime errors"
and use_ocamldep =
flag "-ocamldep" no_arg
~doc:" Use ocamldep. Expect only the main file in the remaining arguments"
and is_verbose =
flag "-verbose" no_arg
~doc:" Be more verbose"
in
fun () ->
let open! Deferred.Let_syntax in
let f compiler =
let loader = loader compiler in
(if use_ocamldep
then
(match plugin_filenames with
| [ main ] -> Dynloader.find_dependencies loader main
| [] | _ :: _ :: _ ->
return
(Or_error.error "Give only the main file when using option -ocamldep"
plugin_filenames [%sexp_of: string list]))
else return (Ok plugin_filenames)
) >>=? fun plugin_filenames ->
if is_verbose then
Print.printf "checking: %s\n%!" (String.concat ~sep:" " plugin_filenames);
if execute_plugin_toplevel
then
load_ocaml_src_files loader plugin_filenames
>>| Or_error.map ~f:ignore
else
check_ocaml_src_files loader plugin_filenames
in
with_compiler ~f ()
>>| function
| Ok () ->
if is_verbose then Print.printf "ok\n%!";
Shutdown.shutdown 0
| Error err ->
Print.eprintf "%s\n%!" (Error.to_string_hum err);
Shutdown.shutdown 1)
;;
module type S = sig
type t
val load_ocaml_src_files : (
string list -> t Deferred.Or_error.t
) create_arguments
val load_ocaml_src_files_without_running_them : (
string list -> (unit -> t) Deferred.Or_error.t
) create_arguments
val check_ocaml_src_files : (
string list -> unit Deferred.Or_error.t
) create_arguments
val check_plugin_cmd : unit -> Command.t
module Load : Dynloader.S with type t := t
end
module Make (X:Dynloader.Module_type) = struct
module Load = Dynloader.Make(X)
let load_ocaml_src_files = make_load_ocaml_src_files Load.load_ocaml_src_files
let load_ocaml_src_files_without_running_them =
make_load_ocaml_src_files Load.load_ocaml_src_files_without_running_them
let check_ocaml_src_files = make_load_ocaml_src_files Load.check_ocaml_src_files
;;
let check_plugin_cmd =
make_check_plugin_cmd
~check_ocaml_src_files:Load.check_ocaml_src_files
~load_ocaml_src_files:Load.load_ocaml_src_files
;;
end
module Side_effect = struct
module Load = Dynloader.Side_effect
let load_ocaml_src_files = make_load_ocaml_src_files Load.load_ocaml_src_files
let load_ocaml_src_files_without_running_them =
make_load_ocaml_src_files Load.load_ocaml_src_files_without_running_them
let check_ocaml_src_files = make_load_ocaml_src_files Load.check_ocaml_src_files
;;
let check_plugin_cmd =
make_check_plugin_cmd
~check_ocaml_src_files:Load.check_ocaml_src_files
~load_ocaml_src_files:Load.load_ocaml_src_files
;;
end