From 91ee6f4546f65d91b34b1d853424c45ffc768739 Mon Sep 17 00:00:00 2001 From: Andrii Sultanov Date: Fri, 14 Nov 2025 15:17:05 +0000 Subject: [PATCH 1/6] quicktest: Add delta tests for VHD data integrity suite These generate two different VDIs (base and leaf), export the difference, import the diff file into the base VDI and check if it's the same as the leaf. Signed-off-by: Andrii Sultanov --- .../quicktest_vdi_ops_data_integrity.ml | 143 +++++++++++++++--- 1 file changed, 119 insertions(+), 24 deletions(-) diff --git a/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml b/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml index 5b385d9b34..fa269005d8 100644 --- a/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml +++ b/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml @@ -76,53 +76,103 @@ let check_vdi_unchanged rpc session_id ~vdi_size ~prepare_vdi ~vdi_op sr_info () ) ) +let check_vdi_delta rpc session_id ~vdi_size ~prepare_vdi ~prepare_vdi_base + ~vdi_op sr_info () = + let sR = sr_info.Qt.sr in + Qt.VDI.with_new ~virtual_size:vdi_size rpc session_id sR + @@ fun vdi_original -> + Qt.VDI.with_new ~virtual_size:vdi_size rpc session_id sR @@ fun base_vdi -> + prepare_vdi rpc session_id vdi_original ; + let checksum_original = checksum rpc session_id vdi_original in + prepare_vdi_base rpc session_id base_vdi ; + + vdi_op rpc session_id ~vdi:vdi_original ~base_vdi ; + let checksum_copy = checksum rpc session_id base_vdi in + if checksum_copy <> checksum_original then + failwith + (Printf.sprintf + "New VDI (checksum: %s) has different data than original (checksum: \ + %s)." + checksum_copy checksum_original + ) + let copy_vdi rpc session_id sr vdi = Client.Client.VDI.copy ~rpc ~session_id ~vdi ~base_vdi:API.Ref.null ~into_vdi:API.Ref.null ~sr -let export_import_vdi rpc session_id ~exportformat sR vdi = - let vdi_uuid = Client.Client.VDI.get_uuid ~rpc ~session_id ~self:vdi in +let export_vdi_to_file ~rpc ~session_id ~exportformat ?base_vdi ~vdi () = + let get_uuid vdi = Client.Client.VDI.get_uuid ~rpc ~session_id ~self:vdi in + let vdi_uuid = get_uuid vdi in + let base_vdi_uuid = Option.map get_uuid base_vdi in let file = "/tmp/quicktest_export_" ^ vdi_uuid in + Qt.cli_cmd + ([ + "vdi-export" + ; "uuid=" ^ vdi_uuid + ; "filename=" ^ file + ; "format=" ^ exportformat + ] + @ match base_vdi_uuid with None -> [] | Some x -> ["base=" ^ x] + ) + |> ignore ; + file + +let create_new_vdi ~rpc ~session_id ~sR ~vdi = + let virtual_size = + Client.Client.VDI.get_virtual_size ~rpc ~session_id ~self:vdi + in + let new_vdi = + Client.Client.VDI.create ~rpc ~session_id ~name_label:"" + ~name_description:"" ~sR ~virtual_size ~_type:`user ~sharable:false + ~read_only:false ~other_config:[] ~xenstore_data:[] ~sm_config:[] ~tags:[] + in + let new_vdi_uuid = + Client.Client.VDI.get_uuid ~rpc ~session_id ~self:new_vdi + in + (new_vdi_uuid, new_vdi) + +let import_file_into_vdi ~file ~vdi_uuid ~exportformat = Qt.cli_cmd [ - "vdi-export" + "vdi-import" ; "uuid=" ^ vdi_uuid ; "filename=" ^ file ; "format=" ^ exportformat ] - |> ignore ; + |> ignore + +let export_import_vdi rpc session_id ~exportformat sR vdi = + let file = export_vdi_to_file ~rpc ~session_id ~exportformat ~vdi () in Xapi_stdext_pervasives.Pervasiveext.finally (fun () -> - let virtual_size = - Client.Client.VDI.get_virtual_size ~rpc ~session_id ~self:vdi - in - let new_vdi = - Client.Client.VDI.create ~rpc ~session_id ~name_label:"" - ~name_description:"" ~sR ~virtual_size ~_type:`user ~sharable:false - ~read_only:false ~other_config:[] ~xenstore_data:[] ~sm_config:[] - ~tags:[] - in - let new_vdi_uuid = - Client.Client.VDI.get_uuid ~rpc ~session_id ~self:new_vdi - in - Qt.cli_cmd - [ - "vdi-import" - ; "uuid=" ^ new_vdi_uuid - ; "filename=" ^ file - ; "format=" ^ exportformat - ] - |> ignore ; + let new_vdi_uuid, new_vdi = create_new_vdi ~rpc ~session_id ~sR ~vdi in + import_file_into_vdi ~file ~vdi_uuid:new_vdi_uuid ~exportformat ; new_vdi ) (fun () -> Sys.remove file) +let export_delta_import_vdi rpc session_id ~exportformat ~vdi ~base_vdi = + let file = + export_vdi_to_file ~rpc ~session_id ~exportformat ~vdi ~base_vdi () + in + Xapi_stdext_pervasives.Pervasiveext.finally + (fun () -> + (* Import delta on top of base_vdi *) + let base_uuid = + Client.Client.VDI.get_uuid ~rpc ~session_id ~self:base_vdi + in + import_file_into_vdi ~file ~vdi_uuid:base_uuid ~exportformat + ) + (fun () -> Sys.remove file) + let export_import_raw = export_import_vdi ~exportformat:"raw" let export_import_vhd = export_import_vdi ~exportformat:"vhd" let export_import_tar = export_import_vdi ~exportformat:"tar" +let delta_export_import_vhd = export_delta_import_vdi ~exportformat:"vhd" + let data_integrity_tests vdi_op op_name = [ ( op_name ^ ": small empty VDI" @@ -141,6 +191,47 @@ let data_integrity_tests vdi_op op_name = ) ] +let delta_data_integrity_tests vdi_op op_name = + [ + ( op_name ^ ": delta between empty & empty VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:noop ~prepare_vdi_base:noop ~vdi_op + ) + ; ( op_name ^ ": delta between random & empty VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:write_random_data ~prepare_vdi_base:noop ~vdi_op + ) + ; ( op_name ^ ": delta between random & random VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:write_random_data ~prepare_vdi_base:write_random_data + ~vdi_op + ) + ; ( op_name ^ ": delta between full and empty VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:fill ~prepare_vdi_base:noop ~vdi_op + ) + ; ( op_name ^ ": delta between full and random VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:fill ~prepare_vdi_base:write_random_data ~vdi_op + ) + ; ( op_name ^ ": delta between full and full VDI" + , `Slow + , check_vdi_delta + ~vdi_size:Sizes.(4L ** mib) + ~prepare_vdi:fill ~prepare_vdi_base:fill ~vdi_op + ) + ] + let large_data_integrity_tests vdi_op op_name = let b = Random.int64 16L in [ @@ -179,6 +270,10 @@ let tests () = @ (data_integrity_tests export_import_vhd "VDI export/import to/from VHD file" |> supported_srs ) + @ (delta_data_integrity_tests delta_export_import_vhd + "VDI delta export/import to/from VHD file" + |> supported_srs + ) @ (data_integrity_tests export_import_tar "VDI export/import to/from TAR file" |> supported_srs ) From 970d678a7374b8fcb4a3ceffccc2126da820800d Mon Sep 17 00:00:00 2001 From: Christian Pardillo Laursen Date: Tue, 25 Nov 2025 11:00:13 +0000 Subject: [PATCH 2/6] CA-421084: xstringext: Improve performance of escaped Signed-off-by: Christian Pardillo Laursen --- .../lib/xapi-stdext-std/xstringext.ml | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.ml b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.ml index 16f60dedba..4e5379d7b3 100644 --- a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.ml +++ b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.ml @@ -42,21 +42,6 @@ module String = struct (** Returns true for whitespace characters, false otherwise *) let isspace = function ' ' | '\n' | '\r' | '\t' -> true | _ -> false - let escaped ?rules string = - match rules with - | None -> - String.escaped string - | Some rules -> - let aux h t = - ( if List.mem_assoc h rules then - List.assoc h rules - else - of_char h - ) - :: t - in - concat "" (fold_right aux string []) - let split_f p str = let split_one seq = let not_p c = not (p c) in @@ -193,6 +178,13 @@ module String = struct ) else s + let escaped ?rules s = + match rules with + | None -> + String.escaped s + | Some rules -> + map_unlikely s (fun c -> List.assoc_opt c rules) + let sub_to_end s start = let length = String.length s in String.sub s start (length - start) From ab74a67a9d1b502db015ddaf3eca523bdde2d34d Mon Sep 17 00:00:00 2001 From: Christian Pardillo Laursen Date: Tue, 25 Nov 2025 14:50:57 +0000 Subject: [PATCH 3/6] xstringext: Test escaped against reference implementation Signed-off-by: Christian Pardillo Laursen --- .../libs/xapi-stdext/lib/xapi-stdext-std/dune | 2 +- .../lib/xapi-stdext-std/xstringext_test.ml | 45 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/dune b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/dune index 67d48233bc..d869973d41 100644 --- a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/dune +++ b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/dune @@ -7,5 +7,5 @@ (names xstringext_test listext_test) (package xapi-stdext-std) (modules xstringext_test listext_test) - (libraries xapi_stdext_std fmt alcotest) + (libraries xapi_stdext_std fmt alcotest qcheck-core qcheck-alcotest) ) diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.ml b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.ml index 145ce632bb..9b7eb2674a 100644 --- a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.ml +++ b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.ml @@ -147,6 +147,49 @@ let test_rtrim = in ("rtrim", List.map test spec) +(** Simple implementation of escaped for testing against *) +let escaped_spec ?rules string = + match rules with + | None -> + String.escaped string + | Some rules -> + let apply_rules char = + match List.assoc_opt char rules with + | None -> + Seq.return char + | Some replacement -> + String.to_seq replacement + in + string |> String.to_seq |> Seq.concat_map apply_rules |> String.of_seq + +let test_escaped = + let open QCheck2 in + (* Generator for escape rules: list of (char, string) mappings *) + let gen_rules = + let open Gen in + let gen_rule = pair char (string_size (int_range 0 5) ~gen:char) in + list gen_rule + in + (* Generator for test input: string and optional rules *) + let gen_input = Gen.pair Gen.string (Gen.opt gen_rules) in + let property (s, rules) = + let expected = escaped_spec ?rules s in + let actual = XString.escaped ?rules s in + String.equal expected actual + in + let test = + Test.make ~name:"escaped matches reference implementation" ~count:1000 + gen_input property + in + ("escaped", [QCheck_alcotest.to_alcotest test]) + let () = Alcotest.run "Xstringext" - [test_rev_map; test_split; test_split_f; test_has_substr; test_rtrim] + [ + test_rev_map + ; test_split + ; test_split_f + ; test_has_substr + ; test_rtrim + ; test_escaped + ] From 8e2b9d852fe31761cfbfca8ef0548e920c060488 Mon Sep 17 00:00:00 2001 From: Christian Pardillo Laursen Date: Tue, 25 Nov 2025 16:20:09 +0000 Subject: [PATCH 4/6] xstringext: Add benchmark for escaped Signed-off-by: Christian Pardillo Laursen --- .../xapi-stdext-std/bench/bench_xstringext.ml | 130 ++++++++++++++++++ .../bench/bench_xstringext.mli | 0 .../lib/xapi-stdext-std/bench/dune | 6 + .../lib/xapi-stdext-std/xstringext_test.mli | 0 quality-gate.sh | 2 +- 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.ml create mode 100644 ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.mli create mode 100644 ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/dune create mode 100644 ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.mli diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.ml b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.ml new file mode 100644 index 0000000000..52897ee2a0 --- /dev/null +++ b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.ml @@ -0,0 +1,130 @@ +open Bechamel +open Toolkit +module XString = Xapi_stdext_std.Xstringext.String + +(* Test data generators *) +let make_string len = String.init len (fun i -> Char.chr (33 + (i mod 94))) + +let escape_rules = + [('a', "[A]"); ('e', "[E]"); ('i', "[I]"); ('o', "[O]"); ('u', "[U]")] + +(* Reference implementation from xstringext_test.ml *) +let escaped_spec ?rules string = + match rules with + | None -> + String.escaped string + | Some rules -> + let apply_rules char = + match List.assoc_opt char rules with + | None -> + Seq.return char + | Some replacement -> + String.to_seq replacement + in + string |> String.to_seq |> Seq.concat_map apply_rules |> String.of_seq + +let escaped_benchmark n = + let s = make_string n in + Staged.stage @@ fun () -> ignore (XString.escaped ~rules:escape_rules s) + +let escaped_spec_benchmark n = + let s = make_string n in + Staged.stage @@ fun () -> ignore (escaped_spec ~rules:escape_rules s) + +let test_escaped = + Test.make_indexed ~name:"escaped" ~fmt:"%s %d" ~args:[100; 500; 1000] + escaped_benchmark + +let test_escaped_spec = + Test.make_indexed ~name:"escaped-spec" ~fmt:"%s %d" ~args:[100; 500; 1000] + escaped_spec_benchmark + +let benchmark () = + let ols = + Analyze.ols ~bootstrap:0 ~r_square:true ~predictors:Measure.[|run|] + in + let instances = + Instance.[minor_allocated; major_allocated; monotonic_clock] + in + let cfg = + Benchmark.cfg ~limit:2000 ~quota:(Time.second 0.5) ~kde:(Some 1000) () + in + let test = + Test.make_grouped ~name:"escaped-comparison" + [test_escaped; test_escaped_spec] + in + let raw_results = Benchmark.all cfg instances test in + let results = + List.map (fun instance -> Analyze.all ols instance raw_results) instances + in + let results = Analyze.merge ols instances results in + (results, raw_results) + +let () = + let all_results = benchmark () in + let results, _ = all_results in + + (* Extract timing data from the actual benchmark results *) + let result_groups = + Hashtbl.fold + (fun _ v a -> Hashtbl.fold (fun k v a -> (k, v) :: a) v [] :: a) + results [] + in + + (* Find the monotonic-clock result group (timing data) *) + let timing_group = + match result_groups with _ :: _ :: timing :: _ -> Some timing | _ -> None + in + + let get_timing test_name = + match timing_group with + | None -> + None + | Some group -> ( + match List.assoc_opt test_name group with + | Some estimator -> ( + let estimates = Analyze.OLS.estimates estimator in + match estimates with Some (x :: _) -> Some x | _ -> None + ) + | None -> + None + ) + in + + Printf.printf "\n=== Performance Comparison: Optimized vs Reference ===\n\n" ; + + let sizes = ["100"; "500"; "1000"] in + List.iter + (fun size -> + Printf.printf "String size %s:\n" size ; + let opt_test = Printf.sprintf "escaped-comparison/escaped %s" size in + let ref_test = Printf.sprintf "escaped-comparison/escaped-spec %s" size in + match (get_timing opt_test, get_timing ref_test) with + | Some opt_time, Some ref_time -> + let improvement = (ref_time -. opt_time) /. ref_time *. 100.0 in + Printf.printf " Optimized: %.3f μs\n" opt_time ; + Printf.printf " Reference: %.3f μs\n" ref_time ; + Printf.printf " Improvement: %.1f%% %s\n\n" improvement + (if improvement > 0.0 then "faster" else "slower") + | None, _ -> + Printf.printf " Optimized implementation data missing\n\n" + | _, None -> + Printf.printf " Reference implementation data missing\n\n" + ) + sizes ; + + Printf.printf "\n=== Detailed Results ===\n" ; + match result_groups with + | [results] -> + let print (k, ols) = Fmt.pr "%s: %a\n%!" k Analyze.OLS.pp ols in + List.iter print results + | results_list -> + Printf.printf "Results structure: %d result groups\n" + (List.length results_list) ; + List.iteri + (fun i results -> + Printf.printf "Result group %d:\n" i ; + let print (k, ols) = Fmt.pr " %s: %a\n%!" k Analyze.OLS.pp ols in + List.iter print results + ) + results_list diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.mli b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.mli new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/dune b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/dune new file mode 100644 index 0000000000..27467a0902 --- /dev/null +++ b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/dune @@ -0,0 +1,6 @@ +(executable + (name bench_xstringext) + (modes exe) + (optional) + (libraries bechamel xapi-stdext-std bechamel-notty notty.unix fmt) +) diff --git a/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.mli b/ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.mli new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quality-gate.sh b/quality-gate.sh index b2345f75ef..6785610ff3 100755 --- a/quality-gate.sh +++ b/quality-gate.sh @@ -25,7 +25,7 @@ verify-cert () { } mli-files () { - N=461 + N=460 X="ocaml/tests" X+="|ocaml/quicktest" X+="|ocaml/message-switch/core_test" From 4fe621cf1684f4653b65a3d5e4b69bb10269c0d6 Mon Sep 17 00:00:00 2001 From: Andrii Sultanov Date: Fri, 14 Nov 2025 11:29:29 +0000 Subject: [PATCH 5/6] quicktest: Add qcow to VDI integrity tests Signed-off-by: Andrii Sultanov --- ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml b/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml index fa269005d8..01d830b878 100644 --- a/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml +++ b/ocaml/quicktest/quicktest_vdi_ops_data_integrity.ml @@ -171,8 +171,12 @@ let export_import_vhd = export_import_vdi ~exportformat:"vhd" let export_import_tar = export_import_vdi ~exportformat:"tar" +let export_import_qcow = export_import_vdi ~exportformat:"qcow2" + let delta_export_import_vhd = export_delta_import_vdi ~exportformat:"vhd" +let delta_export_import_qcow = export_delta_import_vdi ~exportformat:"qcow2" + let data_integrity_tests vdi_op op_name = [ ( op_name ^ ": small empty VDI" @@ -277,6 +281,14 @@ let tests () = @ (data_integrity_tests export_import_tar "VDI export/import to/from TAR file" |> supported_srs ) + @ (data_integrity_tests export_import_qcow + "VDI export/import to/from QCOW file" + |> supported_srs + ) + @ (delta_data_integrity_tests delta_export_import_qcow + "VDI delta export/import to/from QCOW file" + |> supported_srs + ) @ (large_data_integrity_tests export_import_tar "VDI export/import to/from TAR file" |> supported_gfs2_srs From 0227766883747b1302284b6876c17628343457e4 Mon Sep 17 00:00:00 2001 From: Pau Ruiz Safont Date: Fri, 28 Nov 2025 10:03:26 +0000 Subject: [PATCH 6/6] opam: update metadata Some dependencies were missing, it's strange that the CI hadn't picked this up until now (in xs-opam) Signed-off-by: Pau Ruiz Safont --- dune-project | 6 +++++- opam/xapi-debug.opam | 1 + opam/xapi-stdext-zerocheck.opam | 1 + opam/xapi-storage-cli.opam | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dune-project b/dune-project index 9e7f1f9b29..8d329288de 100644 --- a/dune-project +++ b/dune-project @@ -177,7 +177,9 @@ (xapi-idl (= :version)) (xapi-types - (= :version))) + (= :version)) + (xapi-stdext-zerocheck + (= :version))) (synopsis "A CLI for xapi storage services") (description "The CLI allows you to directly manipulate virtual disk images, without them being attached to VMs.")) @@ -322,6 +324,7 @@ xapi-types xapi-stdext-pervasives xapi-stdext-unix + xapi-stdext-zerocheck xen-api-client xen-api-client-lwt xenctrl @@ -874,4 +877,5 @@ (synopsis "Xapi's standard library extension, Zerocheck") (authors "Jonathan Ludlam") (depends + (alcotest :with-test) (odoc :with-doc))) diff --git a/opam/xapi-debug.opam b/opam/xapi-debug.opam index b5a017d9b8..26871b97a7 100644 --- a/opam/xapi-debug.opam +++ b/opam/xapi-debug.opam @@ -58,6 +58,7 @@ depends: [ "xapi-types" "xapi-stdext-pervasives" "xapi-stdext-unix" + "xapi-stdext-zerocheck" "xen-api-client" "xen-api-client-lwt" "xenctrl" diff --git a/opam/xapi-stdext-zerocheck.opam b/opam/xapi-stdext-zerocheck.opam index c347d77214..2d856581f1 100644 --- a/opam/xapi-stdext-zerocheck.opam +++ b/opam/xapi-stdext-zerocheck.opam @@ -8,6 +8,7 @@ homepage: "https://xapi-project.github.io/" bug-reports: "https://github.com/xapi-project/xen-api/issues" depends: [ "dune" {>= "3.20"} + "alcotest" {with-test} "odoc" {with-doc} ] build: [ diff --git a/opam/xapi-storage-cli.opam b/opam/xapi-storage-cli.opam index ee30d9c1c0..c91efa5261 100644 --- a/opam/xapi-storage-cli.opam +++ b/opam/xapi-storage-cli.opam @@ -17,6 +17,7 @@ depends: [ "xapi-client" {= version} "xapi-idl" {= version} "xapi-types" {= version} + "xapi-stdext-zerocheck" {= version} "odoc" {with-doc} ] build: [