Files
meowc/bin/meowc.ml
T
sneeker 752ddea916 feat(sched): cancel in-flight actions when a build fails
Only new spawns were held back after a failure, so every action already running
was allowed to finish even though its output would be discarded. The scheduler
now sends SIGTERM to the remaining children on the first failure, and reports
them separately rather than counting a process it killed as a failure of its
own. Partial outputs are removed and their cache entries dropped, so the next
run starts clean.

Under -k the existing behaviour is kept, since keeping going is the point.

On a graph with three six-second generators alongside one translation unit that
fails immediately, -j4 now returns in 0.12 s rather than 6.03 s.
2022-07-15 04:18:43 +00:00

483 lines
19 KiB
OCaml

open Meow
open Types
let usage =
String.concat "\n"
[
Style.bold "meowc" ^ Style.dim " a build tool for C and C++";
"";
Style.bold " usage";
" meowc [build] [target...] build everything, or the named targets";
" meowc run <bin> [args...] build a bin target, then run it";
" meowc test [filter] build and run test targets";
" meowc install copy installable outputs under the prefix";
" meowc configure run the checks and write the config header";
" meowc check parse and validate, build nothing";
" meowc targets list every target";
" meowc graph [--dot] show the dependency graph";
" meowc explain <file> say why a file is or is not up to date";
" meowc compdb write compile_commands.json";
" meowc watch [target...] rebuild whenever an input changes";
" meowc init <name> scaffold a new project here";
" meowc clean delete the build directory";
"";
Style.bold " options";
" -f FILE build file to read (default build.meow)";
" -j N parallel jobs (default: cores)";
" -C DIR change to DIR first";
" -D name=value set an option declared with 'option'";
" --prefix DIR install prefix (default /usr/local)";
" -k keep going after a failure";
" -n dry run, print what would happen";
" -v print each command as it finishes";
" -q only print warnings and errors";
" --no-color plain output";
]
let cores_from_cpuinfo () =
match Fs.read "/proc/cpuinfo" with
| exception Sys_error _ -> None
| s -> (
match
List.length
(List.filter
(fun l -> String.length l >= 9 && String.sub l 0 9 = "processor")
(String.split_on_char '\n' s))
with
| 0 -> None
| n -> Some n)
let cores_from_getconf () =
match Exec.capture [| "getconf"; "_NPROCESSORS_ONLN" |] with
| 0, out -> int_of_string_opt (String.trim out)
| _ -> None
| exception _ -> None
let cores () =
match cores_from_cpuinfo () with
| Some n -> n
| None -> ( match cores_from_getconf () with Some n when n > 0 -> n | _ -> 4)
let duration s = if s >= 1.0 then Printf.sprintf "%.2f s" s else Printf.sprintf "%.0f ms" (s *. 1000.0)
type flags = {
mutable file : string;
mutable jobs : int;
mutable chdir : string;
mutable defs : (string * string) list;
mutable prefix : string option;
mutable keep : bool;
mutable dry : bool;
mutable verbose : bool;
mutable quiet : bool;
mutable dot : bool;
mutable targets_only : bool;
mutable interval : float;
}
let fl =
{ file = "build.meow"; jobs = 0; chdir = ""; defs = []; prefix = None; keep = false;
dry = false; verbose = false; quiet = false; dot = false; targets_only = false;
interval = 0.25 }
let parse_args argv =
let rest = ref [] in
(* Once "run <target>" is on the positional list, the rest belongs to the
program being run, not to meowc. "--" ends option parsing anywhere. *)
let passthrough () = match !rest with "run" :: _ :: _ -> true | _ -> false in
let rec go = function
| [] -> ()
| "--" :: r when passthrough () -> rest := !rest @ r
| args when passthrough () -> rest := !rest @ args
| "--" :: r -> rest := !rest @ r
| "-f" :: v :: r -> fl.file <- v; go r
| "-j" :: v :: r ->
fl.jobs <- (try int_of_string v with _ -> Diag.error "-j wants a number, got %S" v);
go r
| "-C" :: v :: r -> fl.chdir <- v; go r
| "-D" :: v :: r ->
(match String.index_opt v '=' with
| None -> Diag.error ~hint:"write -D name=value" "malformed -D %S" v
| Some i ->
fl.defs <- fl.defs @ [ (String.sub v 0 i, String.sub v (i + 1) (String.length v - i - 1)) ]);
go r
| "--prefix" :: v :: r -> fl.prefix <- Some v; go r
| "--interval" :: v :: r -> fl.interval <- (try float_of_string v with _ -> 0.25); go r
| "--dot" :: r -> fl.dot <- true; go r
| "--targets" :: r -> fl.targets_only <- true; go r
| "-k" :: r -> fl.keep <- true; go r
| "-n" :: r -> fl.dry <- true; go r
| "-v" :: r -> fl.verbose <- true; go r
| "-q" :: r -> fl.quiet <- true; go r
| "--no-color" :: r -> Style.enabled := false; go r
| ("-h" | "--help") :: _ -> print_endline usage; exit 0
| (("-f" | "-j" | "-C" | "-D" | "--prefix") as o) :: [] -> Diag.error "%s needs a value" o
| a :: r when String.length a > 2 && String.sub a 0 2 = "-j" ->
let v = String.sub a 2 (String.length a - 2) in
fl.jobs <- (try int_of_string v with _ -> Diag.error "-j wants a number, got %S" v);
go r
| a :: r when String.length a > 2 && String.sub a 0 2 = "-D" ->
go ("-D" :: String.sub a 2 (String.length a - 2) :: r)
| a :: r when String.length a > 2 && String.sub a 0 2 = "-f" ->
fl.file <- String.sub a 2 (String.length a - 2);
go r
| a :: r when String.length a > 2 && String.sub a 0 2 = "-C" ->
fl.chdir <- String.sub a 2 (String.length a - 2);
go r
| a :: _ when String.length a > 1 && a.[0] = '-' ->
Diag.error ~hint:"run 'meowc --help' for the full list" "unknown option %s" a
| a :: r -> rest := !rest @ [ a ]; go r
in
go argv;
!rest
let banner p =
if not fl.quiet then begin
Printf.printf "%s %s%s\n\n" (Style.bold "meowc")
(Style.pad 40 (Style.dim fl.file))
(Style.dim p.pname);
flush stdout
end
let configure () =
if not (Sys.file_exists fl.file) then
Diag.error ~hint:"run 'meowc init <name>' to start one" "no %s here" fl.file;
let overrides = Hashtbl.create 8 in
List.iter (fun (k, v) -> Hashtbl.replace overrides k v) fl.defs;
let env = Eval.create ~overrides ~quiet:fl.quiet ~builddir:"build" in
(match fl.prefix with Some p -> Eval.setvar env "prefix" [ p ] | None -> ());
let stmts = Parse.file fl.file in
Eval.run env (Filename.dirname fl.file) stmts;
env.Eval.probe.Probe.path <- Filename.concat env.Eval.tc.builddir ".meowc-probe";
Fs.mkdir_p env.Eval.tc.builddir;
Probe.save env.Eval.probe;
List.iter
(fun (k, _) ->
if not (Hashtbl.mem env.Eval.opts k) then
Diag.error ~hint:"only options declared with 'option' can be set" "no option named %S" k)
fl.defs;
let p = Resolve.project env in
if env.Eval.probes_shown > 0 && not fl.quiet then print_newline ();
p
let with_config_header p =
match Configh.emit p with
| Some (path, true) when not fl.quiet ->
Printf.printf " %s%s\n" (Style.yellow (Style.pad 8 "config")) path
| _ -> ()
let unknown_target p name =
Diag.error
~hint:(Suggest.hint name (List.map (fun (t : target) -> t.name) p.targets))
"no target named %S" name
let select_targets p names =
match names with
| [] -> List.filter (fun (t : target) -> t.kind <> Test) p.targets
| _ -> List.map (fun n -> match find p n with Some t -> t | None -> unknown_target p n) names
let on_start (n : Graph.node) =
if fl.dry then Printf.printf " %s%s\n" (Build.paint n.tag (Style.pad 8 n.tag)) n.label
let on_done ~ok ~log (n : Graph.node) =
if ok then Printf.printf " %s%s\n" (Build.paint n.tag (Style.pad 8 n.tag)) n.label
else
Printf.printf " %s%s\n%s\n" (Style.red (Style.pad 8 "failed")) n.label
(Style.dim (" " ^ Exec.show n.cmd));
if String.trim log <> "" then (print_string log; flush stdout);
flush stdout
let execute p ~names =
let b = Build.of_project p in
let targets = select_targets p names in
let selected = Build.select b targets in
let jobs = if fl.jobs > 0 then fl.jobs else cores () in
if fl.dry then begin
let order = Graph.topo b.g in
let n = ref 0 in
List.iter
(fun i ->
if Hashtbl.mem selected i then begin
let v = b.g.Graph.nodes.(i) in
incr n;
Printf.printf " %s%s\n" (Build.paint v.tag (Style.pad 8 v.tag)) v.label;
if fl.verbose then print_endline (" " ^ Style.dim (Exec.show v.cmd))
end)
order;
Printf.printf "\n %s\n" (Style.dim (Printf.sprintf "%d actions, nothing run" !n));
(b, { Sched.built = 0; cached = 0; failed = 0; aborted = 0 })
end
else begin
let cache = Cache.load (Filename.concat p.tc.builddir ".meowc-cache") in
ignore (Graph.topo b.g);
let r =
Fun.protect
~finally:(fun () -> Cache.save cache)
(fun () ->
Sched.run b.g ~selected ~jobs ~cache ~verbose:fl.verbose ~keep_going:fl.keep ~on_start
~on_done)
in
(b, r)
end
let summarise (r : Sched.result) elapsed =
if not fl.quiet && not fl.dry then begin
let text =
if r.failed > 0 then
Style.red (Style.plural r.failed "action" ^ " failed")
^ (if r.aborted > 0 then Style.dim (Printf.sprintf ", %d cancelled" r.aborted) else "")
else if r.built = 0 then Style.dim "nothing to do"
else Printf.sprintf "%d built%s" r.built (Style.dim (Printf.sprintf ", %d cached" r.cached))
in
Printf.printf "%s %s%s\n"
(if r.built + r.failed + r.aborted = 0 then "" else "\n")
(Style.pad 48 text) (Style.dim (duration elapsed))
end;
if r.failed > 0 then exit 1
let cmd_build names =
let p = configure () in
banner p;
with_config_header p;
let t0 = Unix.gettimeofday () in
let _, r = execute p ~names in
summarise r (Unix.gettimeofday () -. t0);
p
let cmd_run = function
| [] -> Diag.error ~hint:"try: meowc run <bin>" "run needs a target name"
| name :: args ->
let p = configure () in
let t = match find p name with
| Some ({ kind = Bin; _ } as t) -> t
| Some t -> Diag.error "%s is a %s, not a bin" name (kind_name t.kind)
| None -> unknown_target p name
in
banner p;
with_config_header p;
let t0 = Unix.gettimeofday () in
let _, r = execute p ~names:[ name ] in
summarise r (Unix.gettimeofday () -. t0);
let exe = Build.bin_of p t in
if not fl.quiet then Printf.printf "\n %s\n" (Style.dim exe);
flush stdout;
Unix.execv exe (Array.of_list (exe :: args))
let cmd_test filter =
let p = configure () in
banner p;
with_config_header p;
let all = Testrun.tests p in
let chosen =
match filter with
| [] -> all
| fs -> List.filter (fun (t : target) -> List.exists (fun f -> Glob.matches f t.name) fs) all
in
if chosen = [] then begin
Printf.printf " %s\n" (Style.dim "no test targets");
exit 0
end;
let t0 = Unix.gettimeofday () in
let _, r = execute p ~names:(List.map (fun (t : target) -> t.name) chosen) in
if r.failed > 0 then summarise r (Unix.gettimeofday () -. t0);
if r.built > 0 then print_newline ();
let results = List.map (Testrun.run_one p) chosen in
List.iter
(fun (o : Testrun.outcome) ->
Printf.printf " %s%s%s\n"
(if o.ok then Style.green (Style.pad 8 "pass") else Style.red (Style.pad 8 "fail"))
(Style.pad 40 o.name)
(Style.dim (Printf.sprintf "%.0f ms" o.ms));
if not o.ok then begin
print_string o.output;
Printf.printf " %s\n" (Style.dim (Printf.sprintf "exit %d" o.code))
end)
results;
let passed = List.length (List.filter (fun (o : Testrun.outcome) -> o.ok) results) in
let total = List.length results in
let text =
if passed = total then Style.green (Printf.sprintf "%d of %d passed" passed total)
else Style.red (Printf.sprintf "%d of %d passed" passed total)
in
Printf.printf "\n %s%s\n" (Style.pad 48 text)
(Style.dim (duration (Unix.gettimeofday () -. t0)));
if passed <> total then exit 1
let cmd_install () =
let p = cmd_build [] in
let items = Install.execute ~dry:fl.dry p in
print_newline ();
List.iter
(fun (src, dst) ->
Printf.printf " %s%s %s %s\n"
(Style.green (Style.pad 8 (if fl.dry then "would" else "install")))
(Style.pad 30 (Filename.basename src)) (Style.dim "->") (Style.dim dst))
items;
if items = [] then Printf.printf " %s\n" (Style.dim "nothing is marked for installation")
else Printf.printf "\n %s\n" (Style.dim (Style.plural (List.length items) "file" ^ " under " ^ p.prefix))
let cmd_targets () =
let p = configure () in
banner p;
List.iter
(fun (t : target) ->
let tag, paint = Build.tag_of t.kind in
ignore tag;
Printf.printf " %s%s%s%s\n"
(paint (Style.pad 9 (kind_name t.kind)))
(Style.pad 22 t.name)
(Style.pad 20 (Style.dim (Style.plural (List.length (all_srcs t)) "source")))
(Style.dim (if t.uses = [] then "" else "uses " ^ String.concat " " t.uses)))
p.targets;
Printf.printf "\n %s\n" (Style.dim (Style.plural (List.length p.targets) "target"))
let cmd_graph () =
let p = configure () in
if fl.targets_only then print_string (Dot.targets_only p)
else
let b = Build.of_project p in
if fl.dot then print_string (Dot.render b)
else begin
banner p;
let order = Graph.topo b.g in
List.iter
(fun i ->
let v = b.g.Graph.nodes.(i) in
Printf.printf " %s%s\n" (Build.paint v.tag (Style.pad 8 v.tag)) (Style.pad 34 v.label);
List.iter
(fun d -> Printf.printf " %s %s\n" (Style.dim "<-") (Style.dim b.g.Graph.nodes.(d).label))
v.deps)
order;
Printf.printf "\n %s\n"
(Style.dim (Style.plural (Array.length b.g.Graph.nodes) "action"))
end
let cmd_explain = function
| [] -> Diag.error "explain needs a file path"
| path :: _ ->
let p = configure () in
let b = Build.of_project p in
banner p;
(match Graph.producing b.g path with
| None ->
Printf.printf " %s is not produced by this build\n" (Style.bold path);
let users =
Array.to_list b.g.Graph.nodes
|> List.filter (fun (n : Graph.node) -> List.mem path (Sched.inputs_of n))
in
if users <> [] then begin
Printf.printf " %s\n" (Style.dim "it is an input to:");
List.iter (fun (n : Graph.node) -> Printf.printf " %s %s\n" (Build.paint n.tag n.tag) n.label) users
end
| Some i ->
let n = b.g.Graph.nodes.(i) in
let cache = Cache.load (Filename.concat p.tc.builddir ".meowc-cache") in
let fresh = Sched.up_to_date cache n in
Printf.printf " %s%s\n" (Build.paint n.tag (Style.pad 8 n.tag)) (Style.bold n.label);
Printf.printf " %s %s\n" (Style.dim "command") (Style.dim (Exec.show n.cmd));
Printf.printf " %s %s\n" (Style.dim "state ")
(if fresh then Style.green "up to date" else Style.yellow "will rebuild");
if not fresh then begin
if not (List.for_all Sys.file_exists n.outs) then
Printf.printf " %s %s\n" (Style.dim "reason ") "output is missing"
else if Cache.current cache (List.hd n.outs) = None then
Printf.printf " %s %s\n" (Style.dim "reason ") "no cached key for this output"
else Printf.printf " %s %s\n" (Style.dim "reason ") "an input or the command changed"
end;
Printf.printf " %s\n" (Style.dim "inputs");
List.iter
(fun i ->
let d = match Cache.digest_file i with Some d -> String.sub d 0 8 | None -> "missing " in
Printf.printf " %s %s\n" (Style.dim d) i)
(Sched.inputs_of n))
let cmd_compdb () =
let p = configure () in
let b = Build.of_project p in
let path = "compile_commands.json" in
let n = Compdb.write b path in
Printf.printf " %s%s %s\n" (Style.green (Style.pad 8 "wrote")) path
(Style.dim (Style.plural n "entry"))
let cmd_watch names =
let round () =
let p = configure () in
with_config_header p;
let t0 = Unix.gettimeofday () in
let b, r = execute p ~names in
summarise r (Unix.gettimeofday () -. t0);
b
in
let rec loop () =
let b = (try Some (round ()) with Diag.Stop ds -> Diag.print ds; None) in
let files = match b with Some b -> Watch.inputs b | None -> [ fl.file ] in
let files = if List.mem fl.file files then files else fl.file :: files in
Printf.printf "\n %s %s\n" (Style.dim "watching") (Style.dim (Style.plural (List.length files) "file"));
flush stdout;
let changed = Watch.wait ~interval:fl.interval files in
Printf.printf "\n %s %s\n\n" (Style.yellow "changed") (String.concat " " (List.map Filename.basename changed));
flush stdout;
loop ()
in
loop ()
let cmd_clean () =
let p = configure () in
if Sys.file_exists p.tc.builddir then begin
Fs.rm_rf p.tc.builddir;
Printf.printf " %s%s\n" (Style.dim (Style.pad 8 "removed")) p.tc.builddir
end
else Printf.printf " %s\n" (Style.dim "already clean")
let cmd_init = function
| [] -> Diag.error ~hint:"try: meowc init hello" "init needs a project name"
| name :: _ ->
let files = Scaffold.create name in
Printf.printf " %s %s\n\n" (Style.bold "meowc") (Style.dim ("new project " ^ name));
List.iter (fun f -> Printf.printf " %s%s\n" (Style.green (Style.pad 8 "new")) f) files;
Printf.printf "\n %s\n" (Style.dim "now run: meowc run " ^ name)
let cmd_check () =
let p = configure () in
banner p;
let b = Build.of_project p in
ignore (Graph.topo b.g);
let srcs = List.length (List.concat_map all_srcs p.targets) in
Printf.printf " %s\n"
(String.concat (Style.dim ", ")
[ Style.plural (List.length p.targets) "target"; Style.plural srcs "source";
Style.plural (List.length p.rules) "generated file";
Style.plural (Array.length b.g.Graph.nodes) "action" ])
let () =
(try
let rest = parse_args (List.tl (Array.to_list Sys.argv)) in
if not (Unix.isatty Unix.stdout) then Style.enabled := false;
if fl.chdir <> "" then Sys.chdir fl.chdir;
match rest with
| "build" :: names -> ignore (cmd_build names)
| "run" :: rest -> cmd_run rest
| "test" :: f -> cmd_test f
| "install" :: _ -> cmd_install ()
| "configure" :: _ ->
let p = configure () in
banner p;
with_config_header p;
Printf.printf " %s\n" (Style.dim "configured")
| "check" :: _ -> cmd_check ()
| "targets" :: _ -> cmd_targets ()
| "graph" :: _ -> cmd_graph ()
| "explain" :: rest -> cmd_explain rest
| "compdb" :: _ -> cmd_compdb ()
| "watch" :: names -> cmd_watch names
| "init" :: rest -> cmd_init rest
| "clean" :: _ -> cmd_clean ()
| "help" :: _ -> print_endline usage
| names -> ignore (cmd_build names)
with
| Diag.Stop ds -> Diag.print ds; exit 1
| Sys_error msg -> prerr_endline (" " ^ Style.red "error" ^ " " ^ msg); exit 1
| Unix.Unix_error (e, f, a) ->
prerr_endline (" " ^ Style.red "error" ^ Printf.sprintf " %s: %s %s" f (Unix.error_message e) a);
exit 1)