Source file opamFilter.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
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2012-2019 OCamlPro                                        *)
(*    Copyright 2012 INRIA                                                *)
(*                                                                        *)
(*  All rights reserved. This file is distributed under the terms of the  *)
(*  GNU Lesser General Public License version 2.1, with the special       *)
(*  exception on linking described in the file LICENSE.                   *)
(*                                                                        *)
(**************************************************************************)

open OpamTypes
open OpamTypesBase
open OpamStd.Op

module OpamParser = OpamParser.FullPos
module OpamPrinter = OpamPrinter.FullPos

let log ?level fmt =
  OpamConsole.log "FILTER" ?level fmt
let slog = OpamConsole.slog

type env = full_variable -> variable_contents option

type fident = name option list * variable * (string * string) option

let to_string ?custom t =
  let custom ~context ~paren t =
    match custom with
    | None -> None
    | Some f -> f ~context ~paren t
  in
  let rec aux ?(context=`Or) t =
    let paren ?(cond=false) f =
      if cond || OpamFormatConfig.(!r.all_parens)
      then Printf.sprintf "(%s)" f else f
    in
    match custom ~context ~paren t with
    | Some str -> str
    | None ->
      match t with
      | FBool b    -> string_of_bool b
      | FString s  -> Printf.sprintf "%S" s
      | FIdent (pkgs,var,converter) ->
        OpamStd.List.concat_map "+"
          (function None -> "_" | Some p -> OpamPackage.Name.to_string p) pkgs
        ^ (if pkgs <> [] then ":" else "")
        ^ OpamVariable.to_string var
        ^ (match converter with
            | Some (it,ifu) -> "?"^it^":"^ifu
            | None -> "")
      | FOp(e,s,f) ->
        paren ~cond:(context <> `Or && context <> `And)
          (Printf.sprintf "%s %s %s"
             (aux ~context:`Relop e)
             (OpamPrinter.relop_kind s)
             (aux ~context:`Relop f))
      | FAnd (e,f) ->
        paren ~cond:(context <> `Or && context <> `And)
          (Printf.sprintf "%s & %s" (aux ~context:`And e) (aux ~context:`And f))
      | FOr (e,f)  ->
        paren ~cond:(context <> `Or)
          (Printf.sprintf "%s | %s" (aux e) (aux f))
      | FNot e     ->
        paren ~cond:(context = `Relop)
          (Printf.sprintf "!%s" (aux ~context:`Not e))
      | FDefined e ->
        paren ~cond:(context = `Relop)
          (Printf.sprintf "?%s" (aux ~context:`Defined e))
      | FUndef f -> Printf.sprintf "#undefined(%s)" (aux f)
  in
  aux t

let rec fold_down_left f acc filter = match filter with
  | FOp(l,_,r) | FAnd(l,r) | FOr(l,r) ->
    fold_down_left f (fold_down_left f (f acc filter) l) r
  | FNot x
  | FUndef x
  | FDefined x ->
    fold_down_left f (f acc filter) x
  | FBool _
  | FString _
  | FIdent _ ->
    f acc filter

let rec map_up f = function
  | FOp (l, op, r) -> f (FOp (map_up f l, op, map_up f r))
  | FAnd (l, r) -> f (FAnd (map_up f l, map_up f r))
  | FOr (l, r) -> f (FOr (map_up f l, map_up f r))
  | FNot x -> f (FNot (map_up f x))
  | FUndef x -> f (FUndef (map_up f x))
  | FDefined x -> f (FDefined (map_up f x))
  | (FBool _ | FString _ | FIdent _) as flt -> f flt

(* ["%%"], ["%{xxx}%"], or ["%{xxx"] if unclosed *)
let string_interp_regex =
  let open Re in
  let notclose =
    rep (alt [
        diff notnl (set "}");
        seq [char '}'; alt [diff notnl (set "%"); stop] ]
      ])
  in
  compile (alt [
      str "%%";
      seq [str "%{"; group (greedy notclose); opt (group (str "}%"))];
    ])

let escape_value =
  let rex = Re.(compile @@ set "\\\"") in
  Re.Pcre.substitute ~rex ~subst:(fun s -> "\\"^s)

let escape_expansions =
  Re.replace_string Re.(compile @@ char '%') ~by:"%%"

let escape_strings = map_up @@ function
  | FString s -> FString (escape_expansions s)
  | flt -> flt

let fident_variables = function
  | [], var, _ -> [OpamVariable.Full.global var]
  | pkgs, var, _ ->
    List.map (function
        | Some n -> OpamVariable.Full.create n var
        | None -> OpamVariable.Full.self var) pkgs

let extract_variables_from_string s =
    let rec aux acc pos =
      try
        let ss = Re.exec ~pos string_interp_regex s in
        if Re.Group.test ss 2 then
          aux (Re.Group.get ss 1 :: acc)
            (fst (Re.Group.offset ss 0) + String.length (Re.Group.get ss 0))
        else
          aux acc (pos+1)
      with Not_found -> acc
    in
    aux [] 0

(* extracts variables appearing in interpolations in a string *)
let string_variables s =
  List.fold_left (fun acc s ->
      try fident_variables (filter_ident_of_string_interp s) @ acc
      with Failure _ -> acc)
    [] (extract_variables_from_string s)

let variables filter =
  fold_down_left (fun acc -> function
      | FString s -> string_variables s @ acc
      | FIdent f -> fident_variables f @ acc
      | _ -> acc)
    [] filter

(* Some cast functions on values *)

let value ?default = function
  | FBool b -> B b
  | FString s -> S s
  | FUndef f ->
    (match default with
     | Some d -> d
     | None -> failwith ("Undefined filter value: "^to_string f))
  | e -> raise (Invalid_argument ("filter value: "^to_string e))

let value_string ?default = function
  | FBool b -> string_of_bool b
  | FString s -> s
  | FUndef f ->
    (match default with
     | Some d -> d
     | None -> failwith ("Undefined string filter value: "^to_string f))
  | e -> raise (Invalid_argument ("value_string: "^to_string e))

let value_bool ?default = function
  | FBool b -> b
  | FString "true" -> true
  | FString "false" -> false
  | FUndef f ->
    (match default with
     | Some d -> d
     | None -> failwith ("Undefined boolean filter value: "^to_string f))
  | e ->
    (match default with
     | Some d -> d
     | None -> raise (Invalid_argument ("value_bool: "^to_string e)))

(* Desugars the "enable" pseudo-variable *)
let desugar_fident ((packages,var,converter) as fident) =
  let enable = OpamVariable.of_string "enable" in
  if packages <> [] && var = enable && converter = None then
    packages, OpamVariable.of_string "installed", Some ("enable","disable")
  else fident

(* Resolves an ident to variable contents *)
let resolve_ident_raw ?(no_undef_expand=false) env fident =
  let open OpamStd.Option.Op in
  let packages,var,converter = desugar_fident fident in
  let bool_of_value = function
    | B b -> Some b
    | S s | L [s] ->
      (try Some (bool_of_string s) with Invalid_argument _ -> None)
    | L _ -> None
  in
  let resolve name =
    let var = match name with
      | Some n -> OpamVariable.Full.create n var
      | None -> OpamVariable.Full.self var
    in
    env var
  in
  let value_opt : variable_contents option = match packages with
  | [] -> env (OpamVariable.Full.global var)
  | [name] -> resolve name
  | names ->
    List.fold_left (fun acc name ->
        if acc = Some false then acc else
        match resolve name with
        | Some (B true) -> acc
        | v -> v >>= bool_of_value)
      (Some true) names
    >>| fun b -> B b
  in
  match converter with
  | Some (iftrue, iffalse) ->
    (match value_opt >>= bool_of_value with
     | Some true -> Some (S iftrue)
     | Some false -> Some (S iffalse)
     | None ->
         if no_undef_expand then value_opt else Some (S iffalse)
    )
  | None -> value_opt

(* Resolves [FIdent] to string or bool, using its package and converter
   specification *)
let resolve_ident ?no_undef_expand env fident =
  match resolve_ident_raw ?no_undef_expand env fident with
  | Some (B b) -> FBool b
  | Some (S s) -> FString s
  | Some (L l) -> FString (String.concat " " l)
  | None -> FUndef (FIdent fident)

(* Resolves ["%{x}%"] string interpolations *)
let expand_string_aux ?(partial=false) ?(escape_value=fun x -> x) ?default env text =
  let default fident = match default, partial with
    | None, false -> None
    | Some df, false -> Some (df fident)
    | None, true -> Some (Printf.sprintf "%%{%s}%%" fident)
    | Some df, true -> Some (Printf.sprintf "%%{%s}%%" (df fident))
  in
  let env v =
    if partial then
      match env v with
      | Some (S s) -> Some (S (escape_expansions s))
      | x -> x
    else env v
  in
  let f g =
    let str = Re.Group.get g 0 in
    if str = "%%" then (if partial then "%%" else "%")
    else if not (OpamStd.String.ends_with ~suffix:"}%" str) then
      (log "ERR: Unclosed variable replacement in %S\n" str;
       str)
    else
    let fident = String.sub str 2 (String.length str - 4) in
    resolve_ident ~no_undef_expand:partial env
      (filter_ident_of_string_interp fident)
    |> value_string ?default:(default fident) |> escape_value
  in
  Re.replace string_interp_regex ~f text

let expand_string = expand_string_aux ?escape_value:None

let unclosed_expansions text =
  let re =
    Re.(
      compile (alt [
          str "%%";
          seq [str "%{";
               group (greedy (rep (diff notnl (char '}'))));
               opt (group (str "}%"))];
        ])
    )
  in
  Re.all re text |> OpamStd.List.filter_map @@ fun gr ->
  if Re.Group.test gr 1 && not (Re.Group.test gr 2) then
    Some (Re.Group.offset gr 0, Re.Group.get gr 0)
  else None

let map_variables_in_fident f (_,_,conv as fid) =
  let vars = fident_variables fid in
  match List.map f vars with
  | [] -> assert false
  | v::vars ->
    let var_name = OpamVariable.Full.variable v in
    match OpamVariable.Full.scope v with
    | OpamVariable.Full.Global ->
      if vars <> [] then invalid_arg "OpamFilter.map_variables";
      [], var_name, conv
    | OpamVariable.Full.Package _ | OpamVariable.Full.Self ->
      if (List.exists (fun v -> OpamVariable.Full.variable v <> var_name)
            vars)
      then invalid_arg "OpamFilter.map_variables";
      List.map (fun v -> match OpamVariable.Full.scope v with
          | OpamVariable.Full.Package name -> Some name
          | OpamVariable.Full.Self -> None
          | OpamVariable.Full.Global ->
            invalid_arg "OpamFilter.map_variables")
        (v::vars),
      var_name,
      conv

let map_variables_in_string f =
  expand_string
    ~partial:true
    ~default:(fun fid_string ->
        try
          fid_string |>
          filter_ident_of_string_interp |>
          map_variables_in_fident f |>
          string_of_filter_ident
        with Failure _ -> fid_string)
    (fun _ -> None)

let map_variables f =
  map_up @@ function
  | FIdent fid -> FIdent (map_variables_in_fident f fid)
  | FString s -> FString (map_variables_in_string f s)
  | flt -> flt

let rec distribute_negations ?(neg=false) = function
  | FAnd (f1, f2) ->
    let f1 = distribute_negations ~neg f1 in
    let f2 = distribute_negations ~neg f2 in
    if neg then FOr (f1, f2) else FAnd (f1, f2)
  | FOr (f1, f2) ->
    let f1 = distribute_negations ~neg f1 in
    let f2 = distribute_negations ~neg f2 in
    if neg then FAnd (f1, f2) else FOr (f1, f2)
  | FBool b -> FBool (if neg then not b else b)
  | FOp (f1, op, f2) ->
    FOp (distribute_negations ~neg:false f1,
         (if neg then OpamFormula.neg_relop op else op),
         distribute_negations ~neg:false f2)
  | FNot f -> distribute_negations ~neg:(not neg) f
  | f -> if neg then FNot f else f

let logop1 cstr op = function
  | FUndef f -> FUndef (cstr f)
  | e ->
    try FBool (op (value_bool e))
    with Invalid_argument s -> log "ERR: %s" s; FUndef (cstr e)

let logop2 cstr op absorb e f = match e, f with
  | _, FBool x when x = absorb -> FBool x
  | FBool x, _ when x = absorb -> FBool x
  | FUndef x, FUndef y | FUndef x, y | x, FUndef y -> FUndef (cstr x y)
  | f, g ->
    try FBool (op (value_bool f) (value_bool g))
    with Invalid_argument s -> log "ERR: %s" s; FUndef (cstr f g)

(* Reduce expressions to values *)

let rec reduce_aux ?no_undef_expand ~default_str env =
  let reduce = reduce ?no_undef_expand ~default_str env in
  function
  | FUndef x -> FUndef x
  | FBool b -> FBool b
  | FString s -> FString s
  | FIdent i -> resolve_ident ?no_undef_expand env i
  | FOp (e,relop,f) ->
    (match reduce e, reduce f with
     | FUndef x, FUndef y -> FUndef (FOp (x, relop, y))
     | FUndef x, y -> FUndef (FOp (x, relop, escape_strings y))
     | x, FUndef y -> FUndef (FOp (escape_strings x, relop, y))
     | e,f ->
       FBool (OpamFormula.check_relop relop
                (OpamVersionCompare.compare (value_string e) (value_string f))))
  | FAnd (e,f) ->
    logop2 (fun e f -> FAnd (e,f)) (&&) false (reduce e) (reduce f)
  | FOr (e,f) ->
    logop2 (fun e f -> FOr (e,f)) (||) true (reduce e) (reduce f)
  | FNot e ->
    logop1 (fun e -> FNot e) not (reduce e)
  | FDefined e ->
    match reduce e with
    | FUndef _ -> FBool false
    | _ -> FBool true

and reduce ?no_undef_expand ?(default_str = Some (fun _ -> "")) env e =
  match reduce_aux ?no_undef_expand ~default_str env e with
  | FString s ->
    (try FString (expand_string ?default:default_str env s)
     with Failure _ -> FUndef (FString (expand_string ~partial:true env s)))
  | e -> e

let eval ?default env e = value ?default (reduce env e)

let eval_to_bool ?default env e = value_bool ?default (reduce env e)

let opt_eval_to_bool env opt =
  match opt with
  | None -> true
  | Some e -> value_bool ~default:false (reduce env e)

let eval_to_string ?default env e = value_string ?default (reduce env e)

let partial_eval env flt =
  match reduce ~no_undef_expand:true ~default_str:None env flt with
  | FUndef f -> f
  | f -> escape_strings f

let ident_of_var v =
  (match OpamVariable.Full.scope v with
   | OpamVariable.Full.Global -> []
   | OpamVariable.Full.Self -> [None]
   | OpamVariable.Full.Package p -> [Some p]),
  OpamVariable.Full.variable v, None

let ident_of_string s =
  ident_of_var (OpamVariable.Full.of_string s)

let ident_value ?default env id = value ?default (resolve_ident env id)

let ident_string ?default env id = value_string ?default (resolve_ident env id)

let ident_bool ?default env id = value_bool ?default (resolve_ident env id)

(* Substitute the file contents and specify the source and destination *)
let expand_interpolations_in_file_full env ~src ~dst =
  let ic = OpamFilename.open_in_bin src in
  let oc = OpamFilename.open_out_bin dst in
  (* Determine if the input file parses in opam-file-format *)
  let is_opam_format =
    try
      let _ =
        OpamParser.channel ic (OpamFilename.to_string src)
      in
      true
    with e ->
      OpamStd.Exn.fatal e;
      false
  in
  (* Reset the input for processing *)
  seek_in ic 0;
  let default _ = "" in
  let write = output_string oc in
  let unquoted s = write @@ expand_string ~default env s in
  let quoted s = write @@ expand_string_aux ~escape_value ~default env s in
  let process =
    if is_opam_format then
      fun () -> OpamInterpLexer.main unquoted quoted (Lexing.from_channel ic)
    else
      let rec aux () =
        match input_line ic with
        | s -> unquoted s; output_char oc '\n'; aux ()
        | exception End_of_file -> ()
      in
        aux
  in
  process ();
  close_in ic;
  close_out oc

(* Substitute the file contents *)
let expand_interpolations_in_file env file =
  let file = OpamFilename.of_basename file in
  let src = OpamFilename.add_extension file "in" in
  expand_interpolations_in_file_full env ~src ~dst:file

(* Apply filters and interpolations to package commands *)

let arguments env (a,f) =
  if opt_eval_to_bool env f then
    match a with
    | CString s -> [expand_string ~default:(fun _ -> "") env s]
    | CIdent i ->
      let fident = filter_ident_of_string i in
      match resolve_ident_raw env fident with
      | Some (S s) -> [s]
      | Some (B b) -> [string_of_bool b]
      | Some (L sl) -> sl
      | None -> log "ERR in replacement: undefined ident %S" i; [""]
  else
    []

let command env (l, f) =
  if opt_eval_to_bool env f then
    match List.concat (List.map (arguments env) l) with
    | [] -> None
    | l  -> Some l
  else
    None

let commands env l = OpamStd.List.filter_map (command env) l

let single_command env l = List.concat (List.map (arguments env) l)

let simple_arg_variables = function
  | CString s -> string_variables s
  | CIdent i ->
    try fident_variables (filter_ident_of_string i)
    with Failure _ -> []

let filter_opt_variables = function
  | None -> []
  | Some f -> variables f
let argument_variables (a,f) =
  simple_arg_variables a @ filter_opt_variables f
let command_variables (l,f) =
  List.fold_left (fun acc a -> argument_variables a @ acc)
    (filter_opt_variables f) l
let commands_variables l =
  List.fold_left (fun acc c -> command_variables c @ acc) [] l

let rec of_formula atom_f = function
  | Empty -> FBool true
  | Atom at -> atom_f at
  | Block f -> of_formula atom_f f
  | And (a, b) -> FAnd (of_formula atom_f a, of_formula atom_f b)
  | Or (a, b) -> FOr (of_formula atom_f a, of_formula atom_f b)

let filter_constraints ?default_version ?default env filtered_constraint =
  OpamFormula.partial_eval
    (function
      | Filter flt ->
        if eval_to_bool ?default env flt then `True else `False
      | Constraint (relop, v) ->
        try
          let v = eval_to_string env v in
          `Formula (Atom (relop, OpamPackage.Version.of_string v))
        with Failure msg -> match default_version with
          | None ->
            log "Warn: ignoring version constraint %a: %s"
              (slog to_string) v msg;
            `Formula (Empty)
          | Some v -> `Formula (Atom (relop, v)))
    filtered_constraint

(* { build & "%{skromuk}%" = "flib%" } *)
(* { build & "flib%%" = "flib%" } *)

let partial_filter_constraints env filtered_constraint =
  OpamFormula.partial_eval
    (function
      | Filter flt ->
        (match partial_eval env flt with
         | FBool true -> `True
         | FBool false -> `False
         | FUndef f | f -> `Formula (Atom (Filter f)))
      | Constraint (relop, flt_v) ->
        (match partial_eval env flt_v with
         | FBool b ->
           `Formula (Atom (Constraint (relop, FString (string_of_bool b))))
         | FUndef f | f ->
           `Formula (Atom (Constraint (relop, f)))))
    filtered_constraint

let gen_filter_formula constraints filtered_formula =
  OpamFormula.map
    (fun (name, fc) -> match constraints fc with
       | `True -> Atom (name, Empty)
       | `False -> Empty
       | `Formula c -> Atom (name, c))
    filtered_formula

let filter_formula ?default_version ?default env ff =
  gen_filter_formula
    (filter_constraints ?default_version ?default env) ff

let partial_filter_formula env ff =
  gen_filter_formula (partial_filter_constraints env) ff

let string_of_filtered_formula =
  let string_of_constraint =
    OpamFormula.string_of_formula (function
        | Constraint (op, FString s) ->
          Printf.sprintf "%s \"%s\"" (OpamPrinter.relop_kind op) s
        | Constraint (op, (FIdent _ as v)) ->
          Printf.sprintf "%s %s" (OpamPrinter.relop_kind op) (to_string v)
        | Constraint (op, v) ->
          Printf.sprintf "%s (%s)" (OpamPrinter.relop_kind op) (to_string v)
        | Filter f -> to_string f)
  in
  OpamFormula.string_of_formula (function
      | n, Empty -> OpamPackage.Name.to_string n
      | n, c ->
        let paren = match c with Atom (Constraint _) -> false | _ -> true in
        Printf.sprintf "%s %s%s%s"
          (OpamPackage.Name.to_string n)
          (if paren then "{" else "")
          (string_of_constraint c)
          (if paren then "}" else ""))

let variables_of_filtered_formula ff =
  OpamFormula.fold_left
    (fun acc (_, f) ->
       OpamFormula.fold_left (fun acc -> function
           | Constraint _ -> acc
           | Filter f -> variables f @ acc)
         acc f)
    [] ff

let deps_var_env ~build ~post ?test ?doc ?dev_setup ?dev var =
  let get_opt = function
    | Some b -> Some (B b)
    | None -> invalid_arg "filter_deps"
  in
  match OpamVariable.Full.to_string var with
  | "build" -> Some (B build)
  | "post" -> Some (B post)
  | "with-test" -> get_opt test
  | "with-doc" -> get_opt doc
  | "with-dev-setup" -> get_opt dev_setup
  | "dev" -> get_opt dev
  | _ -> None

let filter_deps
    ~build ~post ?test ?doc ?dev_setup ?dev ?default_version ?default
    deps =
  filter_formula ?default_version ?default
    (deps_var_env ~build ~post ?test ?doc ?dev_setup ?dev) deps

let rec simplify_extended_version_formula ef =
  let to_pure ef =
    try
      Some (OpamFormula.map (function
          | Constraint (op, FString s) when string_variables s = [] ->
            Atom (op, OpamPackage.Version.of_string s)
          | _ -> failwith "Impure")
          ef)
    with Failure _ -> None
  in
  let to_filtered =
    OpamFormula.map (fun (op, v) ->
        Atom (Constraint (op, FString (OpamPackage.Version.to_string v))))
  in
  match to_pure ef with
  | Some f ->
    OpamStd.Option.map to_filtered (OpamFormula.simplify_version_formula f)
  | None -> match ef with
    | And _ | Or _ ->
      let conj = match ef with And _ -> true | _ -> false in
      let l = OpamFormula.(if conj then ands_to_list else ors_to_list) ef in
      (try
         let filtered, pure =
           List.fold_left (fun (filtered, pure) ef1 ->
               match to_pure ef1 with
               | Some f -> filtered, f::pure
               | None ->
                 let ef1 = simplify_extended_version_formula ef1 in
                 match ef1 with
                 | None -> (* Always false *)
                   if conj then failwith "false" else filtered, pure
                 | Some ef1 ->
                   (match to_pure ef1 with
                    | Some f -> filtered, f::pure
                    | None -> ef1::filtered, pure))
             ([], []) l
         in
         let mk = OpamFormula.(if conj then ands else ors) in
         match OpamFormula.simplify_version_formula (mk pure) with
         | None -> if conj then None else Some (mk (List.rev filtered))
         | Some pure ->
           Some (mk (List.rev_append filtered [to_filtered pure]))
       with Failure _ -> None)
    | Block ef -> simplify_extended_version_formula ef
    | atom -> Some atom

let atomise_extended =
  OpamFormula.map (fun (x, c) ->
      match c with
      | Empty -> Atom (x, (FBool true, None))
      | cs ->
        let rec aux filters = function
          | Atom (Filter f) -> Atom (x, (FAnd (f,filters), None))
          | Atom (Constraint c) -> Atom (x, (filters, Some c))
          | Empty ->
            (match filters with FBool true -> Empty | f -> Atom (x, (f, None)))
          | Block f -> aux filters f
          | And _ as f ->
            let filters, constraints =
              let rec split filters conj = function
                | Atom (Filter f) :: r -> split (FAnd (f,filters)) conj r
                | cstr :: r -> split filters (cstr::conj) r
                | [] -> filters, conj
              in
              split filters [] (OpamFormula.ands_to_list f)
            in
            OpamFormula.ands (List.rev_map (aux filters) constraints)
          | Or (a, b) -> Or (aux filters a, aux filters b)
        in
        aux (FBool true) cs)

let sort_filtered_formula compare ff =
  let f = OpamFormula.sort compare ff in
  let rec vc_sort = function
    | Empty -> Empty
    | Atom (n,vf) ->
      Atom (n, (OpamStd.Option.default vf
                  (simplify_extended_version_formula vf)))
    | Block f -> Block (vc_sort f)
    | And (f,f') -> And (vc_sort f, vc_sort f')
    | Or (f,f') -> Or (vc_sort f, vc_sort f')
  in
  vc_sort f