C build tool of the 21st century
at main 250 lines 7.9 kB view raw
1let lock = Mutex.create () 2let spinner_frames = [| ""; ""; ""; "" |] 3let spinner_idx = ref 0 4 5type log_level = [ `Quiet | `Info | `Debug ] 6 7let log_level = function 0 -> `Quiet | 1 -> `Info | _ -> `Debug 8let is_verbose = function `Quiet -> false | `Info | `Debug -> true 9let is_debug = function `Debug -> true | _ -> false 10 11type progress_state = { 12 mutable current : int; 13 total : int; 14 is_tty : bool; 15 mutable current_file : string; 16} 17 18let progress : progress_state option Atomic.t = Atomic.make None 19 20let init_progress total = 21 if Unix.isatty Unix.stderr && total > 0 then 22 Atomic.set progress 23 (Some { current = 0; total; is_tty = true; current_file = "" }) 24 25let finalize_progress () = 26 match Atomic.exchange progress None with 27 | Some p when p.is_tty -> 28 (* Clear the progress line *) 29 Fmt.epr "\r\027[K%!" 30 | _ -> () 31 32let clear_progress_bar () = 33 match Atomic.get progress with 34 | Some p when p.is_tty -> 35 (* Clear current line *) 36 Fmt.epr "\r\027[K%!"; 37 flush stderr 38 | _ -> () 39 40let terminal_width = ref None 41 42let terminal_width ?(force = false) () = 43 if force then terminal_width := None; 44 match !terminal_width with 45 | Some w -> w 46 | None -> 47 let width = 48 match Sys.getenv_opt "COLUMNS" with 49 | Some cols -> int_of_string cols 50 | None -> ( 51 try 52 let ic = Unix.open_process_in "tput cols" in 53 let w = int_of_string (input_line ic) in 54 close_in ic; 55 w 56 with _ -> 80) 57 in 58 terminal_width := Some width; 59 width 60 61let truncate_left ?(max_len = 30) s = 62 if String.length s > max_len then 63 String.sub s (String.length s - max_len) max_len 64 else s 65 66let truncate_path_left ?max_len s = 67 let s = Eio.Path.native_exn s in 68 truncate_left ?max_len s 69 70let redraw_progress_bar () = 71 match Atomic.get progress with 72 | Some p when p.is_tty && p.current < p.total && p.current > 0 -> 73 (* Don't redraw if we're at 100% or haven't started - it will be finalized soon *) 74 let frame = 75 spinner_frames.(!spinner_idx mod Array.length spinner_frames) 76 in 77 let percent = if p.total > 0 then p.current * 100 / p.total else 0 in 78 let bar_width = 20 in 79 let filled = bar_width * percent / 100 in 80 let bar = 81 String.concat "" 82 (List.init bar_width (fun i -> if i < filled then "" else "")) 83 in 84 (* Calculate available space for filename based on terminal width *) 85 let term_width = terminal_width () in 86 (* Format without filename to calculate fixed width *) 87 let fixed_part = 88 Printf.sprintf "%s [%s] %d%% (%d/%d) " frame bar percent p.current 89 p.total 90 in 91 let fixed_width = String.length fixed_part in 92 let available_for_file = max 20 (term_width - fixed_width - 1) in 93 let file_display = 94 truncate_left ~max_len:available_for_file p.current_file 95 in 96 Fmt.epr "%s%s%!" fixed_part file_display 97 | _ -> () 98 99let log_clear ?(verbose = true) fmt = 100 Fmt.kstr 101 (fun msg -> 102 Mutex.protect lock @@ fun () -> 103 if verbose then ( 104 clear_progress_bar (); 105 Fmt.epr "%s@." msg; 106 redraw_progress_bar ())) 107 fmt 108 109let log ?(verbose = true) fmt = 110 Mutex.protect lock @@ fun () -> 111 if verbose then Fmt.epr (fmt ^^ "@.") else Fmt.kstr ignore fmt 112 113let log_error ~log_output ~filepath ~target ?command ?exn () = 114 Mutex.protect lock @@ fun () -> 115 clear_progress_bar (); 116 Fmt.epr "\n%s@." log_output; 117 Fmt.epr "compilation failed for '%s' in target '%s'@." filepath target; 118 let () = 119 match command with None -> () | Some cmd -> Fmt.epr "command: %s@." cmd 120 in 121 let () = 122 match exn with Some e -> Fmt.epr "\tmessage: %a@." Fmt.exn e | None -> () 123 in 124 redraw_progress_bar () 125 126let log_spinner ?(verbose = true) fmt = 127 if verbose then Mutex.protect lock @@ fun () -> Fmt.epr ("" ^^ fmt ^^ "@.") 128 else 129 Fmt.kstr 130 (fun msg -> 131 Mutex.protect lock @@ fun () -> 132 match Atomic.get progress with 133 | Some p -> 134 p.current <- p.current + 1; 135 p.current_file <- msg; 136 let frame = 137 spinner_frames.(!spinner_idx mod Array.length spinner_frames) 138 in 139 incr spinner_idx; 140 let percent = 141 if p.total > 0 then p.current * 100 / p.total else 0 142 in 143 let bar_width = 25 in 144 let filled = bar_width * percent / 100 in 145 let bar = 146 String.concat "" 147 (List.init bar_width (fun i -> if i < filled then "" else "")) 148 in 149 (* Calculate available space for filename based on terminal width *) 150 let term_width = terminal_width () in 151 let fixed_part = 152 Printf.sprintf "\r\027[K%s [%s] %d%% (%d/%d) " frame bar percent 153 p.current p.total 154 in 155 let fixed_width = String.length fixed_part - 5 in 156 (* -5 for escape codes *) 157 let available_for_file = max 20 (term_width - fixed_width - 1) in 158 let file_display = truncate_left ~max_len:available_for_file msg in 159 Fmt.epr "%s%s%!" fixed_part file_display 160 | None -> ()) 161 fmt 162 163let ext path = 164 let s = 165 Eio.Path.split path |> Option.map snd |> Option.value ~default:"" 166 |> Filename.extension 167 in 168 if String.length s > 0 then String.sub s 1 (String.length s - 1) else "" 169 170let with_ext path ext = 171 let a, b = Eio.Path.split path |> Option.get in 172 let c = Filename.remove_extension b ^ "." ^ ext in 173 Eio.Path.(a / c) 174 175let mkparent path = 176 let parent = Eio.Path.split path |> Option.map fst in 177 Option.iter 178 (fun p -> 179 if Eio.Path.native_exn p = "." || Eio.Path.native_exn p = "" then () 180 else if not (Eio.Path.is_directory p) then 181 Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 p) 182 parent 183 184let relative_to base a = 185 let prefix = Eio.Path.native_exn base in 186 let a = Eio.Path.native_exn a in 187 if String.starts_with ~prefix a then 188 let prefix_len = String.length prefix in 189 String.sub a (prefix_len + 1) (String.length a - prefix_len - 1) 190 else a 191 192let glob = 193 Re.Glob.glob ~pathname:true ~anchored:true ~double_asterisk:true 194 ~expand_braces:true 195 196let uname = 197 let ic = Unix.open_process_in "uname -s" in 198 Fun.protect ~finally:(fun () -> close_in_noerr ic) @@ fun () -> 199 String.trim @@ input_line ic 200 201let is_static_lib (filename : string) = 202 String.starts_with ~prefix:"lib" filename 203 && String.ends_with ~suffix:".a" filename 204 || String.ends_with ~suffix:".cmxa" filename 205 206let is_shared_lib (filename : string) = 207 String.starts_with ~prefix:"lib" filename 208 && (String.ends_with ~suffix:".so" filename 209 || String.ends_with ~suffix:".dylib" filename) 210 211let normalize_shared_lib_ext (filename : string) = 212 if Sys.os_type = "Unix" && String.ends_with ~suffix:".so" filename then 213 try 214 if uname = "Darwin" then Filename.remove_extension filename ^ ".dylib" 215 else filename 216 with _ -> filename 217 else filename 218 219let parse_gitignore path = 220 if Eio.Path.is_file path then 221 Eio.Path.with_lines path @@ fun lines -> 222 Seq.filter_map 223 (fun line -> 224 let line = String.trim line in 225 if String.length line = 0 || String.starts_with ~prefix:"#" line then 226 None 227 else 228 let pattern = 229 if String.starts_with ~prefix:"/" line then 230 String.sub line 1 (String.length line - 1) 231 else line 232 in 233 Some (glob pattern)) 234 lines 235 |> List.of_seq 236 else [] 237 238let extension_is_c_or_cxx = function 239 | "c" | "cc" | "cpp" | "cxx" -> true 240 | _ -> false 241 242let remove_duplicates_preserving_order lst = 243 let seen = Hashtbl.create (List.length lst) in 244 List.fold_left 245 (fun acc x -> 246 if not (Hashtbl.mem seen x) then 247 let () = Hashtbl.add seen x () in 248 x :: acc 249 else acc) 250 [] lst