Source file b0_cmd_build.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
open B0_std
open B0_std.Result.Syntax
let warn_dup_tool use ign n =
Log.warn @@ fun m ->
m "@[<v>Tool %a defined both by unit %a and %a.@,\
Ignoring definition in unit %a.@]"
Fmt.(code string) n B0_unit.pp_name use
B0_unit.pp_name ign B0_unit.pp_name ign
let warn_no_exe_file u n =
Log.warn @@ fun m ->
m "@[<v>Tool %a defined by unit %a does not specify a@,\
B0_meta.exe_file key. It will not be used in the build (if needed).@]"
Fmt.(code string) n B0_unit.pp_name u
let tool_lookup ~may_build ~must_build ~env =
let tool_name_map =
let add_unit u acc =
match B0_meta.find B0_meta.exe_name (B0_unit.meta u) with
| None -> acc
| Some t ->
match String.Map.find_opt t acc with
| Some u' -> warn_dup_tool u u' t; acc
| None ->
if B0_meta.mem B0_meta.exe_file (B0_unit.meta u)
then String.Map.add t u acc
else (warn_no_exe_file u t; acc)
in
B0_unit.Set.fold add_unit (B0_unit.Set.union may_build must_build)
String.Map.empty
in
let lookup = B00.Memo.tool_lookup_of_os_env env in
fun m t -> match String.Map.find_opt (Fpath.to_string t) tool_name_map with
| None -> lookup m t
| Some u ->
Fut.map Result.ok (B0_meta.get B0_meta.exe_file (B0_unit.meta u))
let memo c ~may_build ~must_build =
let hash_fun = B0_driver.Conf.hash_fun c in
let cwd = B0_driver.Conf.cwd c in
let cache_dir = B0_driver.Conf.cache_dir c in
let b0_dir = B0_driver.Conf.b0_dir c in
let trash_dir = Fpath.(b0_dir / B00_cli.Memo.trash_dir_name) in
let jobs = B0_driver.Conf.jobs c in
let feedback =
let op_howto ppf o = Fmt.pf ppf "b0 log --id %d" (B000.Op.id o) in
let show_op = Log.Info and show_ui = Log.Error and level = Log.level () in
B00_cli.Memo.pp_leveled_feedback ~op_howto ~show_op ~show_ui ~level
Fmt.stderr
in
let* env = Os.Env.current () in
let tool_lookup = tool_lookup ~may_build ~must_build ~env in
B00.Memo.memo
~hash_fun ~cwd ~tool_lookup ~env ~cache_dir ~trash_dir ~jobs ~feedback ()
let units_of ~units ~packs =
let pack_units = List.concat_map B0_pack.units packs in
B0_unit.Set.of_list (List.rev_append units pack_units)
let get_excluded_units ~x_units ~x_packs =
let* units = B0_unit.get_list_or_hint x_units in
let* packs = B0_pack.get_list_or_hint x_packs in
Ok (units_of ~units ~packs)
let get_must_units_and_locked_packs ~units ~packs =
let* units, packs = match units, packs with
| [], [] ->
begin match B0_pack.find "default" with
| None -> Ok (B0_unit.list (), [])
| Some t -> Ok ([], [t])
end
| _ ->
let* units = B0_unit.get_list_or_hint units in
let* packs = B0_pack.get_list_or_hint packs in
Ok (units, packs)
in
let locked_packs = List.filter B0_pack.locked packs in
Ok (units_of ~units ~packs, locked_packs)
let is_locked ~lock ~locked_packs = match lock, locked_packs with
| Some false, _ -> false
| None, [] -> false
| _, _ -> true
let get_may_must ~locked ~units ~x_units =
let must = B0_unit.Set.diff units x_units in
let may =
if locked then must else
let all = B0_unit.Set.of_list (B0_unit.list ()) in
B0_unit.Set.diff all x_units
in
may, must
let find_outcome_action ~must_build action args =
let warn_noact u = Log.warn @@ fun m ->
m "No action for unit %a: ignoring arguments." B0_unit.pp_name u
in
let warn_disable u = Log.warn @@ fun m ->
m "Unit action ignored: unit %a must not build, see %a."
B0_unit.pp_name u Fmt.(code string) "--what"
in
match action with
| None -> Ok None
| Some a ->
let* u = B0_unit.get_or_hint a in
match B0_unit.action u with
| None -> warn_noact u; Ok None
| Some act when B0_unit.Set.mem u must_build -> Ok (Some (act, u))
| Some _ -> warn_disable u; Ok None
let do_output_action_path u args =
match B0_unit.find_meta B0_meta.exe_file u with
| Some path ->
let p = Fut.sync path in
Log.app (fun m -> m "%a" Cmd.pp Cmd.(path p %% of_list Fun.id args));
Ok B00_cli.Exit.ok
| None ->
Log.err
(fun m -> m "No executable outcome path found in unit %a"
B0_unit.pp_name u);
Ok B00_cli.Exit.some_error
let build_run'
lock ~units ~packs ~x_units ~x_packs output_action_path action args c
=
let* x_units = get_excluded_units ~x_units ~x_packs in
let* units, locked_packs = get_must_units_and_locked_packs ~units ~packs in
let locked = is_locked ~lock ~locked_packs in
let may_build, must_build = get_may_must ~locked ~units ~x_units in
match B0_unit.Set.is_empty must_build with
| true -> Log.err (fun m -> m "Empty build!"); Ok B0_driver.Exit.build_error
| false ->
let b0_file = Option.get (B0_driver.Conf.b0_file c) in
let root_dir = Fpath.parent b0_file in
let b0_dir = B0_driver.Conf.b0_dir c in
Log.if_error' ~use:B0_driver.Exit.build_error @@
let* m = memo c ~may_build ~must_build in
let variant = "user" in
let build =
B0_build.create ~root_dir ~b0_dir ~variant m ~may_build ~must_build
in
let* action = find_outcome_action ~must_build action args in
match B0_build.run build with
| Error () -> Ok B0_driver.Exit.build_error
| Ok () ->
match action with
| None -> Ok B00_cli.Exit.ok
| Some (action, u) ->
if output_action_path
then do_output_action_path u args
else Ok (Fut.sync (action build u ~args))
let build_run
lock ~units ~packs ~x_units ~x_packs output_action_path action args c
=
let find_action = function
| None -> Ok None
| Some act ->
let u = B0_unit.get_or_suggest act in
let cmdlet = B0_cmdlet.get_or_suggest act in
match u, cmdlet with
| Ok u, Ok cmdlet ->
Fmt.error "Both a tool and cmdlet are called %a"
(Fmt.code Fmt.string) act
| Ok u, Error _ -> Ok (Some (`Unit u))
| Error _, Ok cmdlet -> Ok (Some (`Cmdlet cmdlet))
| Error us, Error cs ->
let us = List.map B0_unit.name us in
let cs = List.map B0_cmdlet.name cs in
let suggs = List.sort String.compare (List.rev_append us cs) in
let kind ppf () = Fmt.string ppf "action" in
let hint = Fmt.did_you_mean in
let pp = Fmt.unknown' ~kind (Fmt.code Fmt.string) ~hint in
Fmt.error "@[%a@]" pp (act, suggs)
in
Log.if_error ~use:B00_cli.Exit.no_such_name @@
let* act = find_action action in
match act with
| Some (`Cmdlet cmdlet) ->
let cmd = B0_cmdlet.cmd cmdlet in
let cwd = B0_driver.Conf.cwd c in
let root_dir = Fpath.parent @@ Option.get @@ B0_driver.Conf.b0_file c in
let scope_dir = B0_def.scope_dir (B0_cmdlet.def cmdlet) in
let scope_dir = Option.value scope_dir ~default:root_dir in
let b0_dir = B0_driver.Conf.b0_dir c in
let exec = B0_cmdlet.Env.v ~cwd ~scope_dir ~root_dir ~b0_dir ~cmdlet in
Ok (cmd exec (Cmd.list args))
| None | Some (`Unit _) ->
build_run'
lock ~units ~packs ~x_units ~x_packs output_action_path action args c
let green = Fmt.(tty_string [`Fg `Green])
let red = Fmt.(tty_string [`Fg `Red])
let log_explain_lock ~locked ~lock ~locked_packs =
let option_reason pre opt ppf = function
| None -> () | Some _ -> Fmt.pf ppf "%s option %a" pre Fmt.(code string) opt
in
let packs_reason lock ppf = function
| [] -> ()
| p :: rest as ps ->
Fmt.pf ppf "%s pack%s %a"
(match lock with Some true -> " and" | _ -> "")
(if rest = [] then "" else "s")
(Fmt.and_enum B0_pack.pp_name) ps
in
match locked with
| true ->
Log.app (fun m ->
m "Build %a by%a%a."
red "locked"
(option_reason "" "--lock") lock
(packs_reason lock) locked_packs);
| false ->
Log.app (fun m ->
m "Build %a%a" green "unlocked" (option_reason " by" "--unlock") lock)
let log_units color ~kind us =
Log.app (fun m ->
m "@[<v1>%a build:@,@[<v>%a@]@]"
color kind Fmt.(list B0_unit.pp_synopsis) (B0_unit.Set.elements us))
let build_what lock ~units ~packs ~x_units ~x_packs c =
Log.if_error ~use:B00_cli.Exit.no_such_name @@
let* x_units = get_excluded_units ~x_units ~x_packs in
let* units, locked_packs = get_must_units_and_locked_packs ~units ~packs in
let locked = is_locked ~lock ~locked_packs in
let may_build, must_build = get_may_must ~locked ~units ~x_units in
Log.if_error' ~use:B00_cli.Exit.some_error @@
let don't = B0_driver.Conf.no_pager c in
let* = B00_pager.find ~don't () in
let* () = B00_pager.page_stdout pager in
match B0_unit.Set.is_empty must_build with
| true -> Log.app (fun m -> m "Empty build."); Ok B00_cli.Exit.ok
| false ->
log_explain_lock ~locked ~lock ~locked_packs;
log_units red ~kind:"Must" must_build;
if not locked then begin
let may_build = B0_unit.Set.diff may_build must_build in
if not (B0_unit.Set.is_empty may_build)
then log_units green ~kind:"May" may_build
end;
Ok B00_cli.Exit.ok
let build
what lock units packs x_units x_packs output_action_path action args c
=
let units = match action with None -> units | Some a -> a :: units in
if what
then build_what lock ~units ~packs ~x_units ~x_packs c
else build_run lock ~units ~packs ~x_units ~x_packs
output_action_path action args c
open Cmdliner
let units = B0_cli.Arg.units ~doc:"Build unit $(docv). Repeatable." ()
let packs = B0_cli.Arg.packs ~doc:"Build pack $(docv). Repeteable." ()
let x_units =
B0_cli.Arg.x_units ()
~doc:"Exclude unit $(docv) from the build. Takes over inclusion."
let x_packs =
B0_cli.Arg.x_packs ()
~doc:"Exclude units in pack $(docv) from the build. Takes over inclusion."
let what =
let doc = "Do not run the build, show units that must and may build." in
Arg.(value & flag & info ["what"] ~doc)
let lock =
let lock =
let doc = "Lock the build to units and packs specified on the cli." in
Some true, Arg.info ["lock"] ~doc
in
let unlock =
let doc = "Unlock a build that contains a locked pack." in
Some false, Arg.info ["unlock"] ~doc
in
Arg.(value & vflag None [lock; unlock])
let output_action_path =
let doc = "Rather than perform action print invocation on $(b,stdout). For \
simple actions this prints the path to the build executable and
is useful if you want to time it without timing the build."
in
Arg.(value & flag & info ["path"] ~doc)
let action =
let doc = "Action to run. Specify it after a $(b,--) otherwise
it gets taken for a $(mname) command when $(b,b0) is used
without a command."
in
Arg.(value & pos 0 (some string) None & info [] ~doc ~docv:"ACTION")
let args =
let doc = "Arguments given as is to the action."
in
Arg.(value & pos_right 0 string [] & info [] ~doc ~docv:"ARG")
let term =
B0_driver.with_b0_file ~driver:B0_b0.driver
Term.(const build $ what $ lock $ units $ packs $ x_units $ x_packs $
output_action_path $ action $ args)
let cmd =
let doc = "Build and run actions (default command)" in
let sdocs = Manpage.s_common_options in
let exits = B0_driver.Exit.infos in
let envs = B00_pager.envs () in
let man_xrefs = [ `Main ] in
let man = [
`S Manpage.s_synopsis;
`P "$(mname) $(tname) \
[$(b,-u) $(i,UNIT)]… [$(b,-p) $(i,PACK)]… [$(i,OPTION)]… \
$(b,--) [$(i,ACTION)] [$(i,ARG)]…";
`S Manpage.s_description;
`P "The $(tname) command builds and runs actions.";
`P "To build a unit use the $(b,-u) option. To build all the units of \
a pack use the $(b,-p) option. If no unit or pack is specified on \
the command line all units build unless a pack named $(b,default) \
exists in the root scope in which case $(b,-p default) is implied.";
`P "Build procedures may dynamically require the build of units \
unspecified on the command line. To prevent a unit from building \
use the $(b,-x) and $(b,-X) options. These options take over \
unit inclusions specified with $(b,-u) and $(b,-p) options.";
`P "If you want to make sure only the exact units you specified are \
in the build, use the $(b,--lock) option to lock the build. \
If you request a pack that has the $(b,B0_meta.locked) tag, \
the build locks automatically unless $(b,--unlock) is specified.";
`P "If you add the $(b,--what) option, the build doesn't run but what must
and may build is shown.";
`P "More background information is available in the manuals, \
see $(b,odig doc b0).";
B0_b0.Cli.man_see_manual; ]
in
Cmd.v (Cmd.info "build" ~doc ~sdocs ~exits ~envs ~man ~man_xrefs) term