Find and remove dead code and unused APIs in OCaml projects
at main 217 lines 7.3 kB view raw
1(* System utilities for prune - TTY detection, dune operations, and project 2 validation *) 3 4open Bos 5module Log = (val Logs.src_log (Logs.Src.create "prune.system") : Logs.LOG) 6 7(* Error helper functions *) 8let err fmt = Fmt.kstr (fun e -> Error (`Msg e)) fmt 9 10let err_no_dune_project root_dir = 11 err "No dune-project file found in %s" root_dir 12 13let err_version_parse version = err "Could not parse OCaml version: %s" version 14 15(* {2 TTY and environment detection} *) 16 17let is_tty () = try Unix.isatty Unix.stdout with Unix.Unix_error _ -> false 18 19(* {2 Dune version checking} *) 20 21let dune_version () = 22 match OS.Cmd.run_out Cmd.(v "dune" % "--version") |> OS.Cmd.out_string with 23 | Ok (version_str, _) -> Some (String.trim version_str) 24 | Error _ -> None 25 26let should_skip_dune_operations = 27 lazy 28 (match Sys.getenv_opt "INSIDE_DUNE" with 29 | None -> false 30 | Some _ -> ( 31 match dune_version () with 32 | Some "3.19.0" -> true 33 | Some version -> 34 Log.debug (fun m -> 35 m "Dune version %s detected, safe to run nested dune commands" 36 version); 37 false 38 | None -> 39 Log.warn (fun m -> 40 m 41 "Could not detect dune version, being conservative and \ 42 skipping nested dune"); 43 true)) 44 45(* {2 OCaml version checking} *) 46 47let ocaml_version () = 48 match OS.Cmd.run_out Cmd.(v "ocaml" % "-version") |> OS.Cmd.out_string with 49 | Ok (version_str, _) -> ( 50 let version_str = String.trim version_str in 51 match String.split_on_char ' ' version_str with 52 | _ :: _ :: _ :: "version" :: version :: _ -> Some version 53 | _ -> None) 54 | Error _ -> None 55 56let parse_version version_str = 57 let extract_number s = 58 match String.split_on_char '+' s with 59 | num :: _ -> ( 60 match String.split_on_char '-' num with num :: _ -> num | [] -> num) 61 | [] -> s 62 in 63 match String.split_on_char '.' version_str with 64 | major :: minor :: patch :: _ -> ( 65 try 66 Some 67 ( int_of_string major, 68 int_of_string minor, 69 int_of_string (extract_number patch) ) 70 with Failure _ -> None) 71 | [ major; minor ] -> ( 72 try Some (int_of_string major, int_of_string minor, 0) 73 with Failure _ -> None) 74 | _ -> None 75 76let check_ocaml_version () = 77 match ocaml_version () with 78 | None -> Error (`Msg "Could not determine OCaml compiler version") 79 | Some version_str -> ( 80 match parse_version version_str with 81 | None -> err_version_parse version_str 82 | Some (major, minor, _patch) -> 83 if major > 5 || (major = 5 && minor >= 3) then Ok () 84 else 85 Error 86 (`Msg 87 (Fmt.str 88 "OCaml compiler version %s is below the minimum required \ 89 version 5.3.0. Please upgrade your OCaml compiler to use \ 90 prune." 91 version_str))) 92 93(* {2 Project validation} *) 94 95let validate_dune_project root_dir = 96 let root_path = Fpath.v root_dir in 97 let dune_project = Fpath.(root_path / "dune-project") in 98 match OS.File.exists dune_project with 99 | Ok false | Error _ -> err_no_dune_project root_dir 100 | Ok true -> Ok () 101 102(* {2 Dune build operations} *) 103 104let run_build_command _cmd_desc build_cmd = 105 Log.debug (fun m -> m "Running: %s" (Cmd.to_string build_cmd)); 106 match 107 OS.Cmd.run_out ~err:OS.Cmd.err_run_out build_cmd |> OS.Cmd.out_string 108 with 109 | Error (`Msg err) -> 110 { Types.success = false; output = err; exit_code = 1; warnings = [] } 111 | Ok (output, (_, status)) -> 112 let exit_code = 113 match status with `Exited n -> n | `Signaled n -> 128 + n 114 in 115 { Types.success = exit_code = 0; output; exit_code; warnings = [] } 116 117let run_single_build root_dir ctx = 118 Log.info (fun m -> m "Running build"); 119 let build_cmd = 120 Cmd.(v "dune" % "build" % "--root" % root_dir % "@all" % "@ocaml-index") 121 in 122 let result = run_build_command "build" build_cmd in 123 let ctx = Types.update_build_result ctx result in 124 let warnings = Warning.parse result.output in 125 let ctx = Types.update_build_result ctx { result with warnings } in 126 (ctx, warnings) 127 128let build_project_and_index root_dir ctx = 129 if Lazy.force should_skip_dune_operations then ( 130 Log.info (fun m -> 131 m 132 "Running inside dune with problematic version - skipping index build \ 133 to avoid deadlock"); 134 Log.warn (fun m -> 135 m "Cross-module detection may be limited without pre-built index"); 136 Ok ()) 137 else 138 let ctx, _warnings = run_single_build root_dir ctx in 139 match Types.last_build_result ctx with 140 | Some result when result.success -> 141 Log.debug (fun m -> m "Build completed successfully"); 142 Ok () 143 | _ -> Error (`Build_failed ctx) 144 145let is_fixable_warning warning_type = 146 match warning_type with 147 | Types.Signature_mismatch | Types.Unbound_field -> true 148 | Types.Unused_value -> true 149 | Types.Unused_type -> true 150 | Types.Unused_open -> true 151 | Types.Unused_constructor -> true 152 | Types.Unused_field -> true 153 | Types.Unnecessary_mutable -> true 154 | _ -> false 155 156let extract_fixable_errors result = 157 let parsed_errors : Types.warning_info list = result.Types.warnings in 158 let fixable_errors = 159 List.filter (fun w -> is_fixable_warning w.Types.warning_type) parsed_errors 160 in 161 Log.debug (fun m -> m "Found %d fixable errors" (List.length fixable_errors)); 162 fixable_errors 163 164let output_excerpt result = 165 if String.length result.Types.output > 1000 then 166 String.sub result.Types.output 0 1000 ^ "\n[... output truncated ...]" 167 else result.Types.output 168 169let classify_build_error ctx = 170 match Types.last_build_result ctx with 171 | None -> Types.Other_errors "No build result available" 172 | Some result when result.success -> Types.No_error 173 | Some result -> 174 Log.debug (fun m -> m "Build failed with output:\n%s" result.output); 175 let fixable_errors = extract_fixable_errors result in 176 if fixable_errors <> [] then Types.Fixable_errors fixable_errors 177 else 178 let output_excerpt = output_excerpt result in 179 Types.Other_errors output_excerpt 180 181let count_all_errors output = 182 let lines = String.split_on_char '\n' output in 183 List.fold_left 184 (fun count line -> 185 let line = String.trim line in 186 if String.length line > 6 && String.sub line 0 6 = "Error:" then count + 1 187 else if 188 Re.execp 189 (Re.compile 190 (Re.seq [ Re.str "characters"; Re.rep Re.any; Re.str ": Error" ])) 191 line 192 then count + 1 193 else count) 194 0 lines 195 196let display_failure_and_exit ctx = 197 let total_error_count = 198 match Types.last_build_result ctx with 199 | Some result -> count_all_errors result.output 200 | None -> 0 201 in 202 Fmt.pr "%a with %d %s - full output:@." 203 Fmt.(styled (`Fg `Red) string) 204 "Build failed" total_error_count 205 (if total_error_count = 1 then "error" else "errors"); 206 let pp_build_error ppf ctx = 207 match Types.last_build_result ctx with 208 | None -> Fmt.pf ppf "No build output available" 209 | Some result -> Fmt.pf ppf "%s" result.output 210 in 211 Fmt.pr "%a@." pp_build_error ctx; 212 let exit_code = 213 match Types.last_build_result ctx with 214 | Some result -> result.exit_code 215 | None -> 1 216 in 217 exit exit_code