Skip to content

Commit 83f0dda

Browse files
authored
Merge pull request #108 from xhtmlboi/attempt-to-erase-files
Add Action `remove_residuals`
2 parents ccecea1 + e80b7f8 commit 83f0dda

File tree

33 files changed

+425
-21
lines changed

33 files changed

+425
-21
lines changed

CHANGES.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
### unreleased
2+
3+
#### Yocaml
4+
5+
- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
6+
7+
#### Yocaml_git
8+
9+
- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
10+
11+
#### Yocaml_unix
12+
13+
- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
14+
15+
#### Yocaml_eio
16+
17+
- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
18+
19+
120
### v2.7.0 2025-11-18 Nantes (France)
221

322
#### Yocaml_git

lib/core/action.ml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ let perform target task ~when_creation ~when_update cache =
6464
let open Eff.Syntax in
6565
let deps, eff, has_dynamic_deps = Task.destruct task in
6666
let* now = Eff.get_time () in
67+
let cache = Cache.mark cache target in
6768
let* interaction = need_update cache has_dynamic_deps deps target in
6869
match interaction with
6970
| Nothing ->
@@ -185,6 +186,9 @@ let batch_list list action cache =
185186
in
186187
cache
187188

189+
let mark_cache on cache path =
190+
match on with `Source -> cache | `Target -> Cache.mark cache path
191+
188192
let restore_cache ?(on = `Target) path =
189193
let open Eff.Syntax in
190194
let* exists = Eff.file_exists ~on path in
@@ -205,7 +209,7 @@ let restore_cache ?(on = `Target) path =
205209
Eff.log ~src:Eff.yocaml_log_src ~level:`Debug
206210
@@ Lexicon.cache_restored path
207211
in
208-
cache)
212+
mark_cache on cache path)
209213
~error:(fun _ ->
210214
let+ () =
211215
Eff.log ~src:Eff.yocaml_log_src ~level:`Warning
@@ -226,6 +230,26 @@ let store_cache ?(on = `Target) path cache =
226230
let* () = Eff.write_file ~on path sexp_str in
227231
Eff.log ~src:Eff.yocaml_log_src ~level:`Debug @@ Lexicon.cache_stored path
228232

233+
let remove_residuals ~target cache =
234+
let on = `Target in
235+
let open Eff.Syntax in
236+
let trace = Cache.trace cache in
237+
let* () =
238+
Eff.logf ~src:Eff.yocaml_log_src ~level:`Info "Remove residuals for %a"
239+
Path.pp target
240+
in
241+
let* target_trace = Trace.from_directory ~on target in
242+
let residuals = Trace.diff ~target:target_trace trace in
243+
let+ _ =
244+
Eff.List.traverse
245+
(fun residual ->
246+
let* () = Eff.erase_file ~on residual in
247+
Eff.logf ~src:Eff.yocaml_log_src ~level:`Info "%a deleted!" Path.pp
248+
residual)
249+
residuals
250+
in
251+
cache
252+
229253
let with_cache ?on path f =
230254
let open Eff in
231255
restore_cache ?on path >>= f >>= store_cache ?on path

lib/core/action.mli

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,20 @@ type t = Cache.t -> Cache.t Eff.t
3232
production phases, an action is a function that takes a cache and returns
3333
the modified cache, wrapped in an effect. *)
3434

35+
val remove_residuals : target:Path.t -> t
36+
(** [remove_residuals ~target] deletes files that were not created by any of the
37+
actions performed. *)
38+
3539
val restore_cache : ?on:Eff.filesystem -> Path.t -> Cache.t Eff.t
3640
(** [restore_cache ?on path] Reads or initiates the cache in a given [path]. *)
3741

3842
val store_cache : ?on:Eff.filesystem -> Path.t -> Cache.t -> unit Eff.t
3943
(** [store_cache ?on path cache] saves the [cache] in a given [path]. *)
4044

4145
val with_cache : ?on:Eff.filesystem -> Path.t -> t -> unit Eff.t
42-
(** [with_cache ?on path f] restores the cache from the given [path], executes
43-
the action [f] using the cache, and then stores the updated cache back to
44-
the same [path].
46+
(** [with_cache ?on ?remove_residuals_in path f] restores the cache from the
47+
given [path], executes the action [f] using the cache, and then stores the
48+
updated cache back to the same [path].
4549
4650
This helps avoid repeating [restore_cache] and [store_cache] calls manually
4751
when chaining multiple cache-aware actions. *)

lib/core/cache.ml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,27 @@ type entry = {
2020
; last_build_date : int option
2121
}
2222

23-
type t = entry Path.Map.t
23+
type t = { entries : entry Path.Map.t; trace : Trace.t }
2424

2525
let entry ?last_build_date hashed_content dynamic_dependencies =
2626
{ hashed_content; dynamic_dependencies; last_build_date }
2727

28-
let empty = Path.Map.empty
29-
let from_list = Path.Map.of_list
28+
let make ~entries ~trace = { entries; trace }
29+
let empty = make ~entries:Path.Map.empty ~trace:Trace.empty
30+
31+
let from_list ?(trace = Trace.empty) entries =
32+
let entries = entries |> Path.Map.of_list in
33+
make ~entries ~trace
3034

3135
let update cache path ?(deps = Deps.empty) ~now content =
3236
let entry = entry ~last_build_date:now content deps in
33-
Path.Map.add path entry cache
37+
{ cache with entries = Path.Map.add path entry cache.entries }
3438

3539
let get cache path =
3640
Option.map
3741
(fun { hashed_content; dynamic_dependencies; last_build_date } ->
3842
(hashed_content, dynamic_dependencies, last_build_date))
39-
(Path.Map.find_opt path cache)
43+
(Path.Map.find_opt path cache.entries)
4044

4145
let entry_to_sexp { hashed_content; dynamic_dependencies; last_build_date } =
4246
let open Sexp in
@@ -70,13 +74,13 @@ let entry_from_sexp sexp =
7074
make hashed_content potential_deps (Some lbd))
7175
| _ -> Error (Sexp.Invalid_sexp (sexp, "cache"))
7276

73-
let to_sexp cache =
77+
let to_sexp { entries; _ } =
7478
Path.Map.fold
7579
(fun key entry acc ->
7680
let k = Path.to_sexp key in
7781
let v = entry_to_sexp entry in
7882
Sexp.node [ k; v ] :: acc)
79-
cache []
83+
entries []
8084
|> Sexp.node
8185

8286
let key_value_from_sexp sexp =
@@ -95,7 +99,8 @@ let from_sexp sexp =
9599
Result.bind acc (fun acc ->
96100
line |> key_value_from_sexp |> Result.map (fun x -> x :: acc)))
97101
(Ok []) entries
98-
|> Result.map Path.Map.of_list
102+
|> Result.map (fun e ->
103+
make ~entries:(Path.Map.of_list e) ~trace:Trace.empty)
99104
| _ -> Error (Sexp.Invalid_sexp (sexp, "cache"))
100105

101106
let entry_equal
@@ -113,17 +118,22 @@ let entry_equal
113118
&& Deps.equal deps_a deps_b
114119
&& Option.equal Int.equal lbd_a lbd_b
115120

116-
let equal = Path.Map.equal entry_equal
121+
let equal { entries; trace } b =
122+
Path.Map.equal entry_equal entries b.entries && Trace.equal trace b.trace
117123

118124
let pp_kv ppf (key, { hashed_content; dynamic_dependencies; last_build_date }) =
119-
Format.fprintf ppf "%a => deps: @[<v 0>%a@]@hash:%s (%a)" Path.pp key Deps.pp
125+
Format.fprintf ppf "%a => deps: @[<v 0>%a@] hash:%s (%a)" Path.pp key Deps.pp
120126
dynamic_dependencies hashed_content
121127
(Format.pp_print_option Format.pp_print_int)
122128
last_build_date
123129

130+
let trace { trace; _ } = trace
131+
let mark cache path = { cache with trace = Trace.add path (trace cache) }
132+
124133
let pp ppf cache =
125-
Format.fprintf ppf "Cache [@[<v 0>%a@]]"
134+
Format.fprintf ppf "Cache [@[<v 0>%a@]]@ @[<v 1>%a@]"
126135
(Format.pp_print_list
127136
~pp_sep:(fun ppf () -> Format.fprintf ppf ";@ ")
128137
pp_kv)
129-
(Path.Map.to_list cache)
138+
(Path.Map.to_list cache.entries)
139+
Trace.pp (trace cache)

lib/core/cache.mli

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ val entry : ?last_build_date:int -> string -> Deps.t -> entry
3434
val empty : t
3535
(** [empty] returns an empty cache. *)
3636

37-
val from_list : (Path.t * entry) list -> t
37+
val from_list : ?trace:Trace.t -> (Path.t * entry) list -> t
3838
(** [from_list l] creates a cache from a list. *)
3939

4040
(** {1 Cache interaction} *)
@@ -47,6 +47,12 @@ val get : t -> Path.t -> (string * Deps.t * int option) option
4747
(** [get cache path] returns the associated hash content, deps set and the last
4848
build date for a given path. *)
4949

50+
val trace : t -> Trace.t
51+
(** Returns the trace of built artifact. *)
52+
53+
val mark : t -> Path.t -> t
54+
(** [mark path] add [path] into the trace. *)
55+
5056
(** {1 Serialization/Deserialization}
5157
5258
Supports serialization and deserialization of cache. *)

lib/core/eff.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ type _ Effect.t +=
109109
| Yocaml_get_mtime : filesystem * Path.t -> int Effect.t
110110
| Yocaml_hash_content : string -> string Effect.t
111111
| Yocaml_write_file : filesystem * Path.t * string -> unit Effect.t
112+
| Yocaml_erase_file : filesystem * Path.t -> unit Effect.t
112113
| Yocaml_is_directory : filesystem * Path.t -> bool Effect.t
113114
| Yocaml_is_file : filesystem * Path.t -> bool Effect.t
114115
| Yocaml_read_dir : filesystem * Path.t -> Path.fragment list Effect.t
@@ -210,6 +211,13 @@ let write_file ~on path content =
210211
let* () = create_directory ~on parent in
211212
perform @@ Yocaml_write_file (on, path, content)
212213

214+
let erase_file ~on path =
215+
let* file = is_file ~on path in
216+
if file then perform @@ Yocaml_erase_file (on, path)
217+
else
218+
logf ~src:yocaml_log_src ~level:`Warning
219+
"%a is not a file (or does not exists)" Path.pp path
220+
213221
let read_directory ~on ?(only = `Both) ?(where = fun _ -> true) path =
214222
let* is_dir = is_directory ~on path in
215223
if is_dir then

lib/core/eff.mli

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ type _ Effect.t +=
258258
transformation). *)
259259
| Yocaml_write_file : filesystem * Path.t * string -> unit Effect.t
260260
(** Effect which describes the writing of a file *)
261+
| Yocaml_erase_file : filesystem * Path.t -> unit Effect.t
262+
(** Effect whicj describes the erasing of a file *)
261263
| Yocaml_is_directory : filesystem * Path.t -> bool Effect.t
262264
(** Effect that returns check if a path is a directory or not. *)
263265
| Yocaml_is_file : filesystem * Path.t -> bool Effect.t
@@ -410,13 +412,17 @@ val write_file : on:filesystem -> Path.t -> string -> unit t
410412
{!val:Yocaml.Eff.create_directory} for creating intermediate directory in
411413
the path. *)
412414

415+
val erase_file : on:filesystem -> Path.t -> unit t
416+
(** [erase_file ~on target] performs the effect [Yocaml_erase_file] that should
417+
erase a file, if the file exists. *)
418+
413419
val is_directory : on:filesystem -> Path.t -> bool t
414420
(** [is_directory ~on target] performs the effect [Yocaml_is_directory] that
415421
should check if a file is a directory or not. *)
416422

417423
val is_file : on:filesystem -> Path.t -> bool t
418-
(** [is_file ~on target] performs the effect [Yocaml_is_directory] and if the
419-
file is not a directory, it return [true], [false] otherwise. *)
424+
(** [is_file ~on target] performs the effect [Yocaml_is_file] and if the file is
425+
not a directory, it return [true], [false] otherwise. *)
420426

421427
val read_directory :
422428
on:filesystem

lib/core/required.mli

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,16 @@ module type RUNTIME = sig
202202
(** [get_time ()] returns the current timestamp. *)
203203

204204
val file_exists : on:[ `Source | `Target ] -> Path.t -> bool t
205-
(** [file_exists ~on:source -> path] returns [true] if the file exists, false
205+
(** [file_exists ~on:source path] returns [true] if the file exists, false
206206
otherwise. *)
207207

208208
val read_file :
209209
on:[ `Source | `Target ] -> Path.t -> (string, runtime_error) result t
210-
(** [read_file ~on:source -> path] returns the content of a file. *)
210+
(** [read_file ~on:source path] returns the content of a file. *)
211+
212+
val erase_file :
213+
on:[ `Source | `Target ] -> Path.t -> (unit, runtime_error) result t
214+
(** [erase_file ~on:source path] erase the given file *)
211215

212216
val get_mtime :
213217
on:[ `Source | `Target ] -> Path.t -> (int, runtime_error) result t

lib/core/runtime.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ module Make (Runtime : Required.RUNTIME) = struct
107107
(function
108108
| Ok x -> continue k x | Error err -> runtimec err)
109109
(Runtime.create_directory ~on:filesystem path))
110+
| Eff.Yocaml_erase_file (filesystem, path) ->
111+
Some
112+
(fun (k : (a, _) continuation) ->
113+
Runtime.bind
114+
(function
115+
| Ok () -> continue k () | Error err -> runtimec err)
116+
(Runtime.erase_file ~on:filesystem path))
110117
| Eff.Yocaml_is_directory (filesystem, path) ->
111118
Some
112119
(fun (k : (a, _) continuation) ->

lib/core/trace.ml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
(* YOCaml a static blog generator.
2+
Copyright (C) 2025 The Funkyworkers and The YOCaml's developers
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>. *)
16+
17+
type t = Path.Set.t
18+
19+
let empty = Path.Set.empty
20+
let add = Path.Set.add
21+
let from_list = Path.Set.of_list
22+
let diff ~target computed = Path.Set.diff target computed |> Path.Set.to_list
23+
24+
let from_directory ~on target =
25+
let open Eff.Syntax in
26+
let rec aux trace parent =
27+
let* children = Eff.read_directory ~on ~only:`Both parent in
28+
Stdlib.List.fold_left
29+
(fun trace child ->
30+
let* trace = trace in
31+
let* as_file = Eff.is_file ~on child in
32+
if as_file then Eff.return (add child trace) else aux trace child)
33+
(Eff.return trace) children
34+
in
35+
aux empty target
36+
37+
let equal = Path.Set.equal
38+
39+
let pp ppf trace =
40+
Format.fprintf ppf "Trace[@[%a@]]"
41+
(Format.pp_print_list (fun ppf p ->
42+
Format.fprintf ppf "%S" (Path.to_string p)))
43+
(trace |> Path.Set.to_list)

0 commit comments

Comments
 (0)