package 0install-solver

  1. Overview
  2. Docs

Source file diagnostics.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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
(* Copyright (C) 2013, Thomas Leonard
 * See the README file for details, or visit http://0install.net.
 *)

(** Explaining why a solve failed or gave an unexpected answer. *)

module List = Solver_core.List

let pf = Format.fprintf

module Make (Results : S.SOLVER_RESULT) = struct
  module Model = Results.Input
  module RoleMap = Results.RoleMap

  let format_role = Model.Role.pp
  let format_restrictions r = String.concat ", " (List.map Model.string_of_restriction r)

  module Note = struct
    type t =
      | UserRequested of Model.restriction
      | ReplacesConflict of Model.Role.t
      | ReplacedByConflict of Model.Role.t
      | Restricts of Model.Role.t * Model.impl * Model.restriction list
      | RequiresCommand of Model.Role.t * Model.impl * Model.command_name
      | Feed_problem of string

    let pp f = function
      | UserRequested r -> pf f "User requested %s" (format_restrictions [r])
      | ReplacesConflict old -> pf f "Replaces (and therefore conflicts with) %a" format_role old
      | ReplacedByConflict replacement -> pf f "Replaced by (and therefore conflicts with) %a" format_role replacement
      | Restricts (other_role, impl, r) ->
        pf f "%a %a requires %s" format_role other_role Model.pp_version impl (format_restrictions r)
      | RequiresCommand (other_role, impl, command) ->
        pf f "%a %a requires '%s' command" format_role other_role Model.pp_version impl (command :> string)
      | Feed_problem msg -> pf f "%s" msg
  end

  (** Represents a single interface in the example (failed) selections produced by the solver.
      It partitions the implementations into good and bad based (initially) on the split from the
      impl_provider. As we explore the example selections, we further filter the candidates. *)
  module Component = struct
    type rejection_reason = [
      | `Model_rejection of Model.rejection
      | `FailsRestriction of Model.restriction
      | `DepFailsRestriction of Model.dependency * Model.restriction
      | `MachineGroupConflict of Model.Role.t * Model.impl
      | `ClassConflict of Model.Role.t * Model.conflict_class
      | `ConflictsRole of Model.Role.t
      | `MissingCommand of Model.command_name
      | `DiagnosticsFailure of string
    ]
    (* Why a particular implementation was rejected. This could be because the model rejected it,
       or because it conflicts with something else in the example (partial) solution. *)

    type reject = Model.impl * rejection_reason

    type t = {
      role : Model.Role.t;
      replacement : Model.Role.t option;
      diagnostics : string Lazy.t;
      selected_impl : Model.impl option;
      selected_commands : Model.command_name list;
      (* orig_good is all the implementations passed to the SAT solver (these are the
         ones with a compatible OS, CPU, etc). They are sorted most desirable first. *)
      orig_good : Model.impl list;
      orig_bad : (Model.impl * Model.rejection) list;
      mutable good : Model.impl list;
      mutable bad : (Model.impl * rejection_reason) list;
      mutable notes : Note.t list;
    }

    (* Initialise a new component.
       @param candidates is the result from the impl_provider.
       @param selected_impl is the selected implementation, or [None] if we chose [dummy_impl].
       @param diagnostics can be used to produce diagnostics as a last resort. *)
    let create
        ~role
        (candidates, orig_bad, feed_problems)
        (diagnostics:string Lazy.t)
        (selected_impl:Model.impl option)
        (selected_commands:Model.command_name list) =
      let {Model.impls; Model.replacement} = candidates in
      let notes = List.map (fun x -> Note.Feed_problem x) feed_problems in
      {
        role;
        replacement;
        orig_good = impls;
        orig_bad;
        good = impls;
        bad = List.map (fun (impl, reason) -> (impl, `Model_rejection reason)) orig_bad;
        notes; diagnostics; selected_impl; selected_commands
      }

    let note t note = t.notes <- note :: t.notes
    let notes t = List.rev t.notes

    (* Did rejecting [impl] make any difference?
       If [t] selected a better version anyway then we don't need to report this rejection. *)
    let affected_selection t impl =
      match t.selected_impl with
      | Some selected when Model.compare_version selected impl > 0 -> false
      | _ -> true

    (* Call [get_problem impl] on each good impl. If a problem is returned, move [impl] to [bad_impls].
       If anything changes and [!note] is not None, report it and clear the pending note. *)
    let filter_impls_ref ~note:n t get_problem =
      let old_good = List.rev t.good in
      t.good <- [];
      old_good |> List.iter (fun impl ->
          match get_problem impl with
          | None -> t.good <- impl :: t.good
          | Some problem ->
            !n |> Option.iter (fun info ->
                if affected_selection t impl then (
                  note t info;
                  n := None;
                )
              );
            t.bad <- (impl, problem) :: t.bad
        )

    let filter_impls ?note t get_problem =
      let note = ref note in
      filter_impls_ref ~note t get_problem

    (* Remove from [good_impls] anything that fails to meet these restrictions.
       Add removed items to [bad_impls], along with the cause. *)
    let apply_restrictions ~note t restrictions =
      let note = ref (Some note) in
      restrictions |> List.iter (fun r ->
          filter_impls_ref ~note t (fun impl ->
              if Model.meets_restriction impl r then None
              else Some (`FailsRestriction r)
            )
        )

    let apply_user_restriction t r =
      note t (UserRequested r);
      (* User restrictions should be applied before reaching the solver, but just in case: *)
      filter_impls t (fun impl ->
          if Model.meets_restriction impl r then None
          else Some (`FailsRestriction r)
        );
      (* Completely remove non-matching impls.
         The user will only want to see the version they asked for. *)
      let new_bad = t.bad |> List.filter (fun (impl, _) ->
          if Model.meets_restriction impl r then true
          else false
        )
      in
      if new_bad <> [] || t.good <> [] then t.bad <- new_bad

    let reject_all t reason =
      t.bad <- List.map (fun impl -> (impl, reason)) t.good @ t.bad;
      t.good <- []

    let replacement t = t.replacement
    let selected_impl t = t.selected_impl
    let selected_commands t = t.selected_commands

    (* When something conflicts with itself then our usual trick of selecting
       the main implementation and failing the dependency doesn't work, so
       special-case that here. *)
    let reject_self_conflicts t =
      filter_impls t (fun impl ->
          let deps, _ = Model.requires t.role impl in
          deps |> List.find_map (fun dep ->
              let { Model.dep_role; _ } = Model.dep_info dep in
              if Model.Role.compare dep_role t.role <> 0 then None else (
                (* It depends on itself. *)
                Model.restrictions dep |> List.find_map (fun r ->
                    if Model.meets_restriction impl r then None
                    else Some (`DepFailsRestriction (dep, r))
                  )
              )
            )
        )

    let finalise t =
      if t.selected_impl = None then (
        reject_self_conflicts t;
        reject_all t (`DiagnosticsFailure (Lazy.force t.diagnostics))
      )

    let pp_reject f ((impl, reason) : reject) =
      match reason with
      | `Model_rejection r -> Format.pp_print_string f (Model.describe_problem impl r)
      | `FailsRestriction r -> pf f "Incompatible with restriction: %s" (Model.string_of_restriction r)
      | `DepFailsRestriction (dep, restriction) ->
          let dep_info = Model.dep_info dep in
          pf f "Requires %a %s" format_role dep_info.Model.dep_role (format_restrictions [restriction])
      | `MachineGroupConflict (other_role, other_impl) ->
          pf f "Can't use %s with selection of %a (%s)"
            (Model.format_machine impl)
            format_role other_role
            (Model.format_machine other_impl)
      | `ClassConflict (other_role, cl) ->
          pf f "In same conflict class (%s) as %a"
            (cl :> string)
            format_role other_role
      | `ConflictsRole other_role -> pf f "Conflicts with %a" format_role other_role
      | `MissingCommand command -> pf f "No %s command" (command : Model.command_name :> string)
      | `DiagnosticsFailure msg -> pf f "Reason for rejection unknown: %s" msg

    let show_rejections ~verbose f rejected =
      let by_version (a, _) (b, _) = Model.compare_version b a in
      let rejected = List.sort by_version rejected in
      let rec aux i = function
        | [] -> ()
        | _ when i = 5 && not verbose -> pf f "@,..."
        | (impl, problem) :: xs ->
          pf f "@,%a: %a" Model.pp_impl_long impl pp_reject (impl, problem);
          aux (i + 1) xs
      in
      aux 0 rejected

    let rejects t =
      let summary =
        if t.orig_good = [] then (
          if t.orig_bad = [] then `No_candidates
          else `All_unusable
        ) else `Conflicts
      in t.bad, summary

    let pp_candidates ~verbose f t =
      if t.selected_impl = None then (
        match rejects t with
        | _, `No_candidates  -> pf f "@,No known implementations at all"
        | bad, `All_unusable -> pf f "@,@[<v2>No usable implementations:%a@]" (show_rejections ~verbose) bad
        | bad, `Conflicts    -> pf f "@,@[<v2>Rejected candidates:%a@]" (show_rejections ~verbose) bad
      )

    let pp_notes f t =
      match notes t with
      | [] -> ()
      | notes -> pf f "@,%a" Format.(pp_print_list ~pp_sep:pp_print_cut Note.pp) notes

    let pp_outcome f t =
      match t.selected_impl with
      | Some sel -> Model.pp_impl_long f sel
      | None -> Format.pp_print_string f "(problem)"

    (* Format a textual description of this component's report. *)
    let pp ~verbose f t =
      pf f "@[<v2>%a -> %a%a%a@]"
        format_role t.role
        pp_outcome t
        pp_notes t
        (pp_candidates ~verbose) t
  end

  type t = Component.t RoleMap.t

  let find_component_ex role report =
    match RoleMap.find_opt role report with
    | Some c -> c
    | None -> failwith (Format.asprintf "Can't find component %a!" format_role role)

  (* Did any dependency of [impl] prevent it being selected?
     This can only happen if a component conflicts with something more important
     than itself (otherwise, we'd select something in [impl]'s interface and
     complain about the dependency instead).

     e.g. A depends on B and C. B and C both depend on D.
     C1 conflicts with D1. The depth-first priority order means we give priority
     to {A1, B1, D1}. Then we can't choose C1 because we prefer to keep D1. *)
  let get_dependency_problem role report impl =
    let check_dep dep =
      let dep_info = Model.dep_info dep in
      match RoleMap.find_opt dep_info.Model.dep_role report with
      | None -> None      (* Not in the selections => can't be part of a conflict *)
      | Some required_component ->
          match Component.selected_impl required_component with
          | None -> None  (* Dummy selection can't cause a conflict *)
          | Some dep_impl ->
              let check_restriction r =
                if Model.meets_restriction dep_impl r then None
                else Some (`DepFailsRestriction (dep, r)) in
              List.find_map check_restriction (Model.restrictions dep) in
    let deps, commands_needed = Model.requires role impl in
    commands_needed |> List.find_map (fun command ->
      if Model.get_command impl command <> None then None
      else Some (`MissingCommand command : Component.rejection_reason)
    )
    |> function
    | Some _ as r -> r
    | None -> List.find_map check_dep deps

  (** A selected component has [dep] as a dependency. Use this to explain why some implementations
      of the required interface were rejected. *)
  let examine_dep requiring_role requiring_impl report dep =
    let {Model.dep_role = other_role; dep_importance = _; dep_required_commands} = Model.dep_info dep in
    match RoleMap.find_opt other_role report with
    | None -> ()
    | Some required_component ->
        let dep_restrictions = Model.restrictions dep in
        if dep_restrictions <> [] then (
          (* Remove implementations incompatible with the other selections *)
          Component.apply_restrictions required_component dep_restrictions
            ~note:(Restricts (requiring_role, requiring_impl, dep_restrictions))
        );

        dep_required_commands |> List.iter (fun command ->
            let note = Note.RequiresCommand (requiring_role, requiring_impl, command) in
            Component.filter_impls ~note required_component (fun impl ->
                if Model.get_command impl command <> None then None
                else Some (`MissingCommand command)
              )
        )

  (* Find all restrictions that are in play and affect this interface *)
  let examine_selection report role component =
    (* Note any conflicts caused by <replaced-by> elements *)
    let () =
      match Component.replacement component with
      | Some replacement when RoleMap.mem replacement report -> (
          Component.note component (ReplacedByConflict replacement);
          Component.reject_all component (`ConflictsRole replacement);
          match RoleMap.find_opt replacement report with
          | Some replacement_component ->
              Component.note replacement_component (ReplacesConflict role);
              Component.reject_all replacement_component (`ConflictsRole role)
          | None -> ()
      )
      | _ -> () in

    match Component.selected_impl component with
    | Some our_impl ->
        (* For each dependency of our selected impl, explain why it rejected impls in the dependency's interface. *)
        let deps, _commands_needed = Model.requires role our_impl in
        (* We can ignore [commands_needed] here because we obviously were selected. *)
        List.iter (examine_dep role our_impl report) deps;
        Component.selected_commands component |> List.iter (fun name ->
            match Model.get_command our_impl name with
            | None -> failwith "BUG: missing command!"    (* Can't happen - it's a "selected" command *)
            | Some command ->
              let deps, _commands_needed = Model.command_requires role command in
              List.iter (examine_dep role our_impl report) deps;
          )
    | None ->
        (* For each of our remaining unrejected impls, check whether a dependency prevented its selection. *)
        Component.filter_impls component (get_dependency_problem role report)

  (* Check for user-supplied restrictions *)
  let examine_extra_restrictions report =
    report |> RoleMap.iter (fun role component ->
      Model.user_restrictions role |> Option.iter (fun restriction ->
        Component.apply_user_restriction component restriction
      )
    )

  (** If we wanted a command on the root, add that as a restriction. *)
  let process_root_req report = function
    | {Model.command = Some root_command; role = root_role} ->
        let component = find_component_ex root_role report in
        Component.filter_impls component (fun impl ->
          if Model.get_command impl root_command <> None then None
          else Some (`MissingCommand root_command)
        )
    | _ -> ()

  (** Find an implementation which requires a machine group. Use this to
      explain the rejection of all implementations requiring other groups. *)
  exception Found of (Model.Role.t * Model.impl * Model.machine_group)
  let check_machine_groups report =
    let check role compoment =
      match Component.selected_impl compoment with
      | None -> ()
      | Some impl ->
          match Model.machine_group impl with
          | None -> ()
          | Some group -> raise (Found (role, impl, group)) in

    try RoleMap.iter check report
    with Found (example_role, example_impl, example_group) ->
      let filter _key component = Component.filter_impls component (fun impl ->
        match Model.machine_group impl with
        | Some group when group <> example_group -> Some (`MachineGroupConflict (example_role, example_impl))
        | _ -> None
      ) in
      RoleMap.iter filter report

  module Classes = Map.Make(struct
      type t = Model.conflict_class
      let compare = compare
    end)

  (** For each selected implementation with a conflict class, reject all candidates
      with the same class. *)
  let check_conflict_classes report =
    let classes =
      RoleMap.fold (fun role component acc ->
          match Component.selected_impl component with
          | None -> acc
          | Some impl -> Model.conflict_class impl |> List.fold_left (fun acc x -> Classes.add x role acc) acc
        ) report Classes.empty
    in
    report |> RoleMap.iter @@ fun role component ->
    Component.filter_impls component @@ fun impl ->
    let rec aux = function
      | [] -> None
      | cl :: cls ->
        match Classes.find_opt cl classes with
        | Some other_role when Model.Role.compare role other_role <> 0 -> Some (`ClassConflict (other_role, cl))
        | _ -> aux cls
    in
    aux (Model.conflict_class impl)

  let of_result result =
    let impls = Results.to_map result in
    let root_req = Results.requirements result in
    let report =
      let get_selected role sel =
        let impl = Results.unwrap sel in
        let diagnostics = lazy (Results.explain result role) in
        let impl = if impl == Model.dummy_impl then None else Some impl in
        let impl_candidates = Model.implementations role in
        let rejects, feed_problems = Model.rejects role in
        let selected_commands = Results.selected_commands sel in
        Component.create ~role (impl_candidates, rejects, feed_problems) diagnostics impl selected_commands in
      RoleMap.mapi get_selected impls
    in
    process_root_req report root_req;
    examine_extra_restrictions report;
    check_machine_groups report;
    check_conflict_classes report;
    RoleMap.iter (examine_selection report) report;
    RoleMap.iter (fun _ c -> Component.finalise c) report;
    report

  let pp_rolemap ~verbose f reasons =
    let pp_item f (_, c) = pf f "- @[%a@]" (Component.pp ~verbose) c in
    Format.(pp_print_list ~pp_sep:pp_print_cut) pp_item f (RoleMap.bindings reasons)

  (** Return a message explaining why the solve failed. *)
  let get_failure_reason ?(verbose=false) result =
    let reasons = of_result result in
    Format.asprintf "Can't find all required implementations:@\n@[<v0>%a@]" (pp_rolemap ~verbose) reasons
end
OCaml

Innovation. Community. Security.