Source file opamDirTrack.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
open OpamStd.Op
open OpamProcess.Job.Op
let log ?level fmt = OpamConsole.log ?level "TRACK" fmt
let slog = OpamConsole.slog
module SM = OpamStd.String.Map
type digest = string
let digest_of_string dg = dg
let string_of_digest dg = dg
type change =
| Added of digest
| Removed
| Contents_changed of digest
| Perm_changed of digest
| Kind_changed of digest
type t = change SM.t
let string_of_change ?(full=false) =
let str s d =
if not full then s else
Printf.sprintf "%s %s" s (string_of_digest d)
in
function
| Added d -> str "addition" d
| Removed -> "removal"
| Contents_changed d -> str "modifications" d
| Perm_changed d -> str "permission change" d
| Kind_changed d -> str "kind change" d
let to_string t =
OpamStd.Format.itemize (fun (f, change) ->
Printf.sprintf "%s of %s"
(String.capitalize_ascii (string_of_change change)) f)
(SM.bindings t)
let to_summary_string t =
let freq_table =
SM.fold (fun _ change ->
SM.union (+) (SM.singleton (string_of_change ~full:false change) 1))
t
SM.empty
in
let freq_list =
OpamStd.List.concat_map ~left:" (" ~right:")" ~nil:"" "; " (fun (change, freq) ->
Printf.sprintf "%s: %d" change freq)
(SM.bindings freq_table)
in
Printf.sprintf "%d items%s"
(SM.cardinal t)
freq_list
(** uid, gid, perm *)
type perms = int * int * int
type item_value =
| File of string
| Dir
| Link of string
| Special of (int * int)
type item = perms * item_value
let cached_digest =
let item_cache = Hashtbl.create 749 in
fun f size mtime ->
try
let csize, cmtime, digest = Hashtbl.find item_cache f in
if csize = size && mtime = cmtime then Digest.to_hex digest
else raise Not_found
with Not_found ->
let digest = Digest.file f in
Hashtbl.replace item_cache f (size, mtime, digest);
Digest.to_hex digest
let quick_digest _f size mtime =
Printf.sprintf "S%dT%s" size (string_of_float mtime)
let get_digest ?(precise=OpamCoreConfig.(!r.precise_tracking)) f size mtime =
if precise then cached_digest f size mtime
else quick_digest f size mtime
let item_of_filename ?precise f : item =
let stats = Unix.lstat f in
Unix.(stats.st_uid, stats.st_gid, stats.st_perm),
match stats.Unix.st_kind with
| Unix.S_REG ->
File (get_digest ?precise f stats.Unix.st_size stats.Unix.st_mtime)
| Unix.S_DIR -> Dir
| Unix.S_LNK -> Link (Unix.readlink f)
| Unix.S_CHR | Unix.S_BLK | Unix.S_FIFO | Unix.S_SOCK ->
Special Unix.(stats.st_dev, stats.st_rdev)
let item_of_filename_opt ?precise f =
try Some (item_of_filename ?precise f)
with Unix.Unix_error _ -> None
let item_digest = function
| _perms, File d -> "F:" ^ d
| _perms, Dir -> "D"
| _perms, Link l -> "L:" ^ l
| _perms, Special (a,b) -> Printf.sprintf "S:%d:%d" a b
let is_precise_digest d =
not (OpamStd.String.starts_with ~prefix:"F:S" d)
let track_t to_track ?(except=OpamFilename.Base.Set.empty) job_f =
let module SM = OpamStd.String.Map in
let rec make_index_topdir acc prefix dir =
let files =
try Sys.readdir (Filename.concat prefix dir)
with Sys_error _ as e ->
log "Error at dir %s: %a" (Filename.concat prefix dir)
(slog Printexc.to_string) e;
[||]
in
Array.fold_left
(fun acc f ->
let rel = Filename.concat dir f in
if OpamFilename.Base.(Set.mem (of_string rel) except) then acc else
let f = Filename.concat prefix rel in
try
let item = item_of_filename f in
let acc = SM.add rel item acc in
match item with
| _, Dir -> make_index_topdir acc prefix rel
| _ -> acc
with Unix.Unix_error _ as e ->
log "Error at %s: %a" f (slog Printexc.to_string) e;
acc)
acc files
in
let make_index =
match to_track with
| `Top dir ->
fun () -> make_index_topdir SM.empty (OpamFilename.Dir.to_string dir) ""
| `Paths (prefix, files) ->
fun () ->
let prefix = OpamFilename.Dir.to_string prefix in
List.fold_left (fun acc f ->
let _, acc =
List.fold_left (fun (path, acc) dir ->
let path =
match path with
| Some path -> Filename.concat path dir
| None -> dir
in
let rel = Filename.concat prefix path in
let item = item_of_filename_opt rel in
let acc =
match item with
| None -> acc
| Some item -> SM.add path item acc
in
Some path, acc)
(None, acc)
(Re.(split (compile (rep1 (str Filename.dir_sep)))) f)
in
acc)
SM.empty files
in
let scan_timer = OpamConsole.timer () in
let before = make_index () in
log ~level:2 "before install: %a elements scanned in %.3fs"
(slog @@ string_of_int @* SM.cardinal) before (scan_timer ());
job_f () @@| fun result ->
let scan_timer = OpamConsole.timer () in
let after = make_index () in
let diff =
SM.merge (fun _ before after ->
match before, after with
| None, None -> assert false
| Some _, None -> Some Removed
| None, Some item -> Some (Added (item_digest item))
| Some (perma, a), Some ((permb, b) as item) ->
if a = b then
if perma = permb then None
else Some (Perm_changed (item_digest item))
else
match a, b with
| File _, File _ | Link _, Link _
| Dir, Dir | Special _, Special _ ->
Some (Contents_changed (item_digest item))
| _ -> Some (Kind_changed (item_digest item)))
before after
in
log "after install: %a elements, %a added, scanned in %.3fs"
(slog @@ string_of_int @* SM.cardinal) after
(slog @@ string_of_int @* SM.cardinal @*
SM.filter (fun _ -> function Added _ -> true | _ -> false))
diff (scan_timer ());
result, diff
let track_files ~prefix files ?except job_f =
track_t (`Paths (prefix, files)) ?except job_f
let track dir ?except job_f =
track_t (`Top dir) ?except job_f
let check_digest file digest =
let precise = is_precise_digest digest in
let it = item_of_filename ~precise file in
try if item_digest it = digest then `Unchanged else `Changed
with Unix.Unix_error _ -> `Removed
let check prefix changes =
let str_pfx = OpamFilename.Dir.to_string prefix in
SM.fold (fun fname op acc ->
let f = Filename.concat str_pfx fname in
match op with
| Added dg | Kind_changed dg | Contents_changed dg ->
(OpamFilename.of_string f, check_digest f dg) :: acc
| Perm_changed _ | Removed -> acc)
changes []
|> List.rev
let revert ?title ?(verbose=OpamConsole.verbose()) ?(force=false)
?(dryrun=false) prefix changes =
let title = match title with
| None -> ""
| Some t -> t ^ ": "
in
let rmdir d = if not dryrun then OpamFilename.rmdir d in
let rmfile f = if not dryrun then OpamFilename.remove f in
let changes =
List.rev (OpamStd.String.Map.bindings changes)
in
let already, modified, nonempty, cannot =
List.fold_left (fun (already,modified,nonempty,cannot as acc) (fname,op) ->
let f = Filename.concat (OpamFilename.Dir.to_string prefix) fname in
match op with
| Added dg | Kind_changed dg ->
let cur_item_ct, cur_dg =
try
let precise = is_precise_digest dg in
let item = item_of_filename ~precise f in
Some (snd item), Some (item_digest item)
with Unix.Unix_error _ -> None, None
in
if cur_dg = None then (fname::already, modified, nonempty, cannot)
else if cur_dg <> Some dg && not force then
(already, fname::modified, nonempty, cannot)
else if cur_item_ct = Some Dir then
let d = OpamFilename.Dir.of_string f in
if OpamFilename.dir_is_empty d then
(rmdir d; acc)
else
let nonempty =
if List.exists
(OpamStd.String.starts_with ~prefix:fname) nonempty
then nonempty else fname::nonempty
in
(already, modified, nonempty, cannot)
else
let f = OpamFilename.of_string f in
rmfile f;
acc
| Contents_changed dg ->
if check_digest f dg = `Changed then
(already, modified, nonempty, (op,fname)::cannot)
else
acc
| (Removed | Perm_changed _) ->
(already, modified, nonempty, (op,fname)::cannot))
([], [], [], []) changes
in
if already <> [] then
log ~level:2 "%sfiles %s were already removed" title
(String.concat ", " (List.rev already));
if modified <> [] then
if OpamConsole.confirm ~default:false
"%sthese files have been modified since installation:\n%s\
Remove them anyway?" title
(OpamStd.Format.itemize (fun s -> s) (List.rev modified)) then
List.iter (fun f -> OpamFilename.remove (OpamFilename.Op.(prefix // f)))
modified;
if nonempty <> [] && verbose then
OpamConsole.note "%snot removing non-empty directories:\n%s" title
(OpamStd.Format.itemize (fun s -> s) (List.rev nonempty));
if cannot <> [] && verbose then
let cannot =
let rem, modf, perm =
List.fold_left (fun (rem, modf, perm as acc) (op,f) ->
match op with
| Removed -> (None, f)::rem, modf, perm
| Contents_changed dg ->
let precise = Some (is_precise_digest dg) in
rem, (precise, f)::modf, perm
| Perm_changed dg ->
let precise = Some (is_precise_digest dg) in
rem, modf, (precise, f)::perm
| _ -> acc)
([],[],[]) cannot
in
(if rem = [] then [] else [Removed, rem])
@ (if modf = [] then [] else [Contents_changed "_", modf])
@ (if perm = [] then [] else [Perm_changed "_", perm])
in
(OpamConsole.warning "%scannot revert:" title;
OpamConsole.errmsg "%s"
(OpamStd.Format.itemize
(fun (op,lf) ->
Printf.sprintf "%s of:\n%s"
(string_of_change op)
(OpamStd.Format.itemize (fun (pre,x) ->
(OpamStd.Option.to_string (fun pr ->
if pr then "[hash] " else "[tms] ") pre) ^ x) lf))
cannot))
let update prefix t =
let removed = ref [] in
let prefix = OpamFilename.Dir.to_string prefix in
let update_digest file digest =
match
let filename = Filename.concat prefix file in
let precise = is_precise_digest digest in
item_digest ( item_of_filename ~precise filename )
with
| exception Unix.Unix_error ( ENOENT, _, _) ->
removed := file :: !removed;
digest
| exception _exn -> digest
| digest -> digest
in
let t =
SM.mapi (fun file change ->
match change with
| Added digest -> Added (update_digest file digest)
| Removed -> Removed
| Contents_changed digest ->
Contents_changed (update_digest file digest)
| Perm_changed digest -> Perm_changed (update_digest file digest)
| Kind_changed digest -> Kind_changed (update_digest file digest)
) t
in
List.fold_left (fun t file ->
SM.remove file t
) t !removed