make.ml 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. (*
  27. * https://doi.org/10.1017/S0956796820000088
  28. * <= https://www.youtube.com/watch?v=MjWx_qfEQXg
  29. * <= https://lobste.rs/s/umdeiu/memo_incremental_computation_library
  30. *)
  31. let ( let* ) = Result.bind
  32. module Jig = struct
  33. let make = String.split_on_char '%'
  34. let cut p s =
  35. let rx = "^" ^ (p |> String.concat {|\(.*\)|}) ^ "$"
  36. |> Str.regexp in
  37. if Str.string_match rx s 0
  38. then (
  39. let n = p |> List.length in
  40. Some (List.init
  41. (n-1)
  42. (fun i -> Str.matched_group (i+1) s)))
  43. else None
  44. let paste jig v : string option =
  45. assert (jig |> List.length <= 1 + (v |> List.length));
  46. match jig with
  47. | [] -> None
  48. | [s]-> Some s (* no % substitutions are totally fine *)
  49. | hd :: tl ->
  50. Some (List.fold_left2 (fun lst a b -> a :: b :: lst) [hd] tl v
  51. |> List.rev
  52. |> String.concat "")
  53. end
  54. type remake =
  55. | Outdated (* remake target if dependencies are more recent *)
  56. | Missing (* remake target ONLY if missing *)
  57. (** a single rule *)
  58. type t = {
  59. target : string; (* should the target be separate to build List.assoc tuples? *)
  60. prerequisites : string list;
  61. fresh : remake;
  62. command : (string -> t list -> t -> string ->
  63. (* return a different name to create a smylink *)
  64. (string, string) result);
  65. }
  66. let src_from (r : t) t =
  67. assert (1 <= (r.prerequisites |> List.length));
  68. let j_src = r.prerequisites |> List.hd |> Jig.make in
  69. let j_dst = r.target |> Jig.make in
  70. assert (3 == (j_src |> List.length));
  71. assert (3 == (j_dst |> List.length));
  72. let v = t |> Jig.cut j_dst |> Option.value ~default:[] in
  73. let src = v |> Jig.paste j_src |> Option.value ~default:"⚠️" in
  74. src,j_dst,v
  75. let dot oc (all : t list) =
  76. (* escape for https://graphviz.org/doc/info/lang.html *)
  77. let esc s =
  78. s
  79. |> String.split_on_char '%'
  80. |> String.concat {|\%|}
  81. in
  82. Printf.fprintf oc "%s"
  83. (esc {|digraph "#Seppo" {
  84. label = "#Seppo files
  85. https://Seppo.Social";
  86. rankdir=TD;
  87. |});
  88. all |> List.fold_left (fun _ r ->
  89. r.prerequisites |> List.fold_left (fun _ p ->
  90. Printf.fprintf oc {| "%s" -> "%s"|} (esc p) (esc r.target);
  91. Printf.fprintf oc "%s" ";\n";
  92. ()) ();
  93. () ) ();
  94. Printf.fprintf oc "%s" "}\n";
  95. Ok ()
  96. module M2 = struct
  97. let match_rule (fn : string) m (r : t) =
  98. match m with
  99. | None ->
  100. let j_dst = r.target |> Jig.make in
  101. (match fn |> Jig.cut j_dst with
  102. | None ->
  103. Logr.debug (fun m -> m "%s.%s %s ~ %s" "Make.M2" "match_rule" fn r.target);
  104. None
  105. | Some v ->
  106. Logr.debug (fun m -> m "%s.%s found %s" "Make.M2" "match_rule" r.target);
  107. Some (r,v))
  108. | Some _ as m -> m
  109. let find_rule rules fn : (t * string list) option =
  110. rules |> List.fold_left (match_rule fn) None
  111. let time ?(default = 0.) fn =
  112. try (Unix.stat fn).st_mtime
  113. with _ -> default
  114. let rec fo_make lvl rules x fn : (float,string) result =
  115. let t0 = Sys.time() in
  116. Logr.debug (fun m -> m "%s.%s %d <%s>" "Make.M2" "fo_make" lvl fn);
  117. let* x' = x in
  118. let r = match fn |> find_rule rules with
  119. | None ->
  120. if fn |> File.exists
  121. then
  122. (Logr.debug (fun m -> m "%s.%s no rule but target exists: %s" "Make.M2" "fo_make" fn);
  123. Ok (time fn))
  124. else Error ("no rule to make target: " ^ fn)
  125. | Some (r,v) ->
  126. let add init v =
  127. match v with
  128. | None -> init
  129. | Some v -> v :: init in
  130. let* t' = r.prerequisites
  131. |> List.fold_left (fun init pq ->
  132. let jig_pq = pq |> Jig.make in
  133. v |> Jig.paste jig_pq |> add init) []
  134. |> List.fold_left (fo_make (lvl+1) rules) x in
  135. let tf = time fn in
  136. if tf >= t'
  137. then
  138. (Logr.debug (fun m -> m "%s.%s up to date: %s" "Make.M2" "fo_make" fn);
  139. Ok (tf |> max x'))
  140. else
  141. let _ = fn |> Filename.dirname |> File.mkdir_p File.pDir in
  142. let* _ = fn |> r.command "-" rules r in
  143. (* create a softlink in case? *)
  144. Ok (time fn)
  145. in
  146. Logr.info (fun m -> m "%s.%s %s dt=%.3fs" "Make.M2" "fo_make" fn (Sys.time() -. t0));
  147. r
  148. let make rules fn =
  149. match fo_make 0 rules (Ok 1.0) fn with
  150. | Error _ as e -> e
  151. | Ok _ -> Ok fn
  152. end
  153. module M1 = struct
  154. let make ?(pre = "") (rs : t list) (fn : string) : (string, 'a) result =
  155. let module MakeMapT = struct
  156. type t = String.t
  157. let compare a b = String.compare a b
  158. end in
  159. let module MakeMap = Map.Make(MakeMapT) in
  160. let rec make' pre (rm : t MakeMap.t) fn =
  161. let t0 = Sys.time() in
  162. let ( >>= ) = Result.bind in
  163. let find_rule fn : t option = MakeMap.find_opt fn rm in (* TODO should we block anything starting with / or containing .. ? *)
  164. match fn |> find_rule with
  165. | None -> (
  166. match File.mtime_0 fn <= 0. with
  167. | true -> Error ("no rule to make target `" ^ fn ^ "'")
  168. | false -> Ok fn)
  169. | Some ({target; prerequisites; fresh; command} as rule) ->
  170. assert (target = fn);
  171. let rec visit_siblings num sibs _ =
  172. match sibs with
  173. | [] -> Ok ""
  174. | hd :: tl ->
  175. Logr.debug (fun m -> m "%s%s.%s visit_sibling %d %s" pre "Make" "make" num hd);
  176. make' (pre ^ " ") rm hd (* depth first *)
  177. >>= visit_siblings (succ num) tl
  178. in
  179. if match fresh with
  180. | Missing -> target |> File.exists
  181. | Outdated ->
  182. (* @TODO handle missing target rules Error *)
  183. let _ = visit_siblings 0 prerequisites "" in
  184. let tta = File.mtime_0 target in
  185. let rec is_fresh = function
  186. | [] -> true
  187. | hd :: tl ->
  188. let b = (hd |> File.mtime_0 <= tta) in
  189. Logr.debug (fun m -> m "%s %s %s is %s than %s" pre (if b then "ok" else "!!") target (if b then "FRESHER" else "OLDER") hd);
  190. b && (is_fresh tl)
  191. in
  192. is_fresh prerequisites
  193. then
  194. Ok target
  195. else (
  196. Logr.debug (fun m -> m "%s %s building" pre target);
  197. let* _ = target |> Filename.dirname |> File.mkdir_p File.pDir in
  198. let r = match target |> command pre rs rule
  199. with
  200. | Error e' as e ->
  201. Logr.err (fun m -> m "%s %s.%s %s: %s" E.e1019 "Make" "make" target e');
  202. e
  203. | Ok re ->
  204. if not (re = "" || re |> String.equal target)
  205. then
  206. Unix.(
  207. Logr.debug (fun m -> m "%s.%s 0 ls -s %s %s" "Make" "make" re target);
  208. (try unlink target;
  209. with | Unix_error(ENOENT, "unlink", _) -> ());
  210. try
  211. Logr.debug (fun m -> m "%s.%s 1 ln -s %s %s" "Make" "make" re target);
  212. let up = re |> St.updir in
  213. symlink (up ^ re) target;
  214. Logr.debug (fun m -> m "%s.%s 2 ln -s %s %s" "Make" "make" (up ^ re) target);
  215. with | e -> Logr.err (fun m -> m "%s %s.%s %a" E.e1030 "Make" "make" St.pp_exc e);
  216. );
  217. Ok target in
  218. Logr.info (fun m -> m "%s.%s %s dt=%.3fs" "Make" "make" fn (Sys.time() -. t0));
  219. r
  220. )
  221. in
  222. let rm = rs |> List.fold_left (fun init r -> MakeMap.add r.target r init) MakeMap.empty in
  223. fn |> make' pre rm
  224. end
  225. let copy dst src : t =
  226. { target = dst;
  227. prerequisites = [ src ];
  228. fresh = Outdated;
  229. command = fun _ _ _ t ->
  230. File.(src |> in_channel (fun ic ->
  231. let _ = t |> Filename.dirname |> mkdir_p pDir in
  232. t |> out_channel_replace (fun oc ->
  233. ic |> copy_channel oc;
  234. Ok t)
  235. ))
  236. }
  237. (** Manage actions from dependencies, file/timestamp based.
  238. Similar POSIX make.
  239. pre: prefix
  240. rs: rules
  241. fn: the file to keep up to date
  242. *)
  243. let make ?(pre = "") (rs : t list) (fn : string) : (string, 'a) result =
  244. (if false
  245. then M2.make
  246. else M1.make ~pre) rs fn