[Libguestfs] [PATCH] builder: complete architecture handling

Pino Toscano
Mon, 10 Mar 2014 13:28:20 -0000

Add the possibility to choose which architecture use to build the wanted
image (--arch). Since this implies that running commands on the guest is
usually not possible when the architecture is different than the host
one, another new option (--allow-foreign-arch-ops) allows to run
commands nevertheless.

The caching scheme is adapted to account for the architecture (with
--print-cache showing the architecture as well).

Furthermore, the short output of --list shows the architecture as well,
like the other --list outputs do.
---
 builder/Makefile.am               |  9 +++++-
 builder/architecture.ml           | 32 +++++++++++++++++++++
 builder/builder.ml                | 28 ++++++++++--------
 builder/cmdline.ml                | 29 +++++++++++++++++--
 builder/downloader.ml             |  8 +++---
 builder/downloader.mli            | 10 +++----
 builder/index_parser.ml           | 22 ++++++++++----
 builder/list_entries.ml           |  2 ++
 builder/test-virt-builder-list.sh | 14 ++++-----
 builder/test-virt-builder.sh      |  1 +
 builder/uname-c.c                 | 60 +++++++++++++++++++++++++++++++++++++++
 builder/uname.ml                  | 27 ++++++++++++++++++
 builder/uname.mli                 | 28 ++++++++++++++++++
 builder/virt-builder.pod          | 35 +++++++++++++++++++----
 po/POTFILES                       |  1 +
 po/POTFILES-ml                    |  2 ++
 16 files changed, 264 insertions(+), 44 deletions(-)
 create mode 100644 builder/architecture.ml
 create mode 100644 builder/uname-c.c
 create mode 100644 builder/uname.ml
 create mode 100644 builder/uname.mli

diff --git a/builder/Makefile.am b/builder/Makefile.am
index 387913c..5a978e3 100644
--- a/builder/Makefile.am
+++ b/builder/Makefile.am
@@ -38,6 +38,7 @@ CLEANFILES = *~ *.cmi *.cmo *.cmx *.cmxa *.o virt-builder

 # Alphabetical order.
 SOURCES = \
+	architecture.ml \
 	builder.ml \
 	cmdline.ml \
 	downloader.mli \
@@ -61,7 +62,10 @@ SOURCES = \
 	sigchecker.mli \
 	sigchecker.ml \
 	sources.mli \
-	sources.ml
+	sources.ml \
+	uname.ml \
+	uname.mli \
+	uname-c.c

 man_MANS  noinst_DATA @@ -99,6 +103,9 @@ deps = \
 	pxzcat.cmx \
 	setlocale-c.o \
 	setlocale.cmx \
+	uname-c.o \
+	uname.cmx \
+	architecture.cmx \
 	ini_reader.cmx \
 	paths.cmx \
 	get_kernel.cmx \
diff --git a/builder/architecture.ml b/builder/architecture.ml
new file mode 100644
index 0000000..2aa7e7e
--- /dev/null
+++ b/builder/architecture.ml
@@ -0,0 +1,32 @@
+(* virt-builder
+ * Copyright (C) 2014 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Unix
+
+let filter_arch = function
+  | "amd64" | "x86_64" | "x64" ->
"x86_64"
+  | "powerpc" | "ppc" -> "ppc"
+  | "armhfp" | "armhf" -> "armhf"
+  | arch -> arch
+
+let current_arch +  try filter_arch ((Uname.uname ()).Uname.machine)
+  with Unix_error _ -> "unknown"
diff --git a/builder/builder.ml b/builder/builder.ml
index 1800f2d..ed55de1 100644
--- a/builder/builder.ml
+++ b/builder/builder.ml
@@ -38,9 +38,9 @@ let () = Random.self_init ()
 let main ()    (* Command line argument parsing - see cmdline.ml. *)
   let mode, arg,
-    attach, cache, check_signature, curl, debug, delete, delete_on_failure,
-    edit, firstboot, run, format, gpg, hostname, install, list_format, links,
-    memsize, mkdirs,
+    arch, attach, cache, check_signature, curl, debug, delete,
+    delete_on_failure, edit, firstboot, run, format, gpg, hostname, install,
+    list_format, links, memsize, mkdirs,
     network, output, password_crypto, quiet, root_password, scrub,
     scrub_logfile, selinux_relabel, size, smp, sources, sync, timezone,
     update, upload, writes @@ -172,11 +172,11 @@ let main ()        | Some
cachedir ->
         printf (f_"cache directory: %s\n") cachedir;
         List.iter (
-          fun (name, { Index_parser.revision = revision; hidden = hidden })
->
+          fun (name, { Index_parser.revision = revision; arch = arch; hidden =
hidden }) ->
             if not hidden then (
-              let filename = Downloader.cache_of_name cachedir name revision in
+              let filename = Downloader.cache_of_name cachedir name arch
revision in
               let cached = Sys.file_exists filename in
-              printf "%-24s %s\n" name
+              printf "%-24s %-10s %s\n" name arch
                 (if cached then s_"cached" else (*s_*)"no")
             )
         ) index
@@ -193,7 +193,7 @@ let main ()          List.iter (
           fun (name,
                { Index_parser.revision = revision; file_uri = file_uri }) ->
-            let template = name, revision in
+            let template = name, arch, revision in
             msg (f_"Downloading: %s") file_uri;
             let progress_bar = not quiet in
             ignore (Downloader.download ~prog downloader ~template
~progress_bar
@@ -205,12 +205,16 @@ let main ()      | (`Install|`Notes) as mode -> mode in

   (* Which os-version (ie. index entry)? *)
-  let entry -    try List.assoc arg index
+  let item +    try List.find (
+      fun (name, { Index_parser.arch = a }) ->
+        name = arg && arch = Architecture.filter_arch a
+    ) index
     with Not_found ->
-      eprintf (f_"%s: cannot find os-version '%s'.\nUse --list to
list available guest types.\n")
-        prog arg;
+      eprintf (f_"%s: cannot find os-version '%s' with
architecture '%s'.\nUse --list to list available guest types.\n")
+        prog arg arch;
       exit 1 in
+  let entry = snd item in
   let sigchecker = entry.Index_parser.sigchecker in

   (match mode with
@@ -234,7 +238,7 @@ let main ()    let template      let template,
delete_on_exit        let { Index_parser.revision = revision; file_uri =
file_uri } = entry in
-      let template = arg, revision in
+      let template = arg, arch, revision in
       msg (f_"Downloading: %s") file_uri;
       let progress_bar = not quiet in
       Downloader.download ~prog downloader ~template ~progress_bar file_uri in
diff --git a/builder/cmdline.ml b/builder/cmdline.ml
index 6e8bfd8..0dd0ad2 100644
--- a/builder/cmdline.ml
+++ b/builder/cmdline.ml
@@ -44,6 +44,9 @@ let parse_cmdline ()    let print_cache_mode () = mode :=
`Print_cache in
   let delete_cache_mode () = mode := `Delete_cache in

+  let arch = ref "" in
+  let allow_foreign_arch_ops = ref false in
+
   let attach = ref [] in
   let attach_format = ref None in
   let set_attach_format = function
@@ -221,6 +224,9 @@ let parse_cmdline ()
   let ditto = " -\"-" in
   let argspec = Arg.align [
+    "--allow-foreign-arch-ops", Arg.Set allow_foreign_arch_ops,
+                                            " " ^ s_"Allow
executing commands even on non-native output architectures";
+    "--arch",    Arg.Set_string arch,       "arch" ^ "
" ^ s_"Set the output architecture";
     "--attach",  Arg.String attach_disk,    "iso" ^ "
" ^ s_"Attach data disk/ISO during install";
     "--attach-format",  Arg.String set_attach_format,
                                             "format" ^ " "
^ s_"Set attach disk format";
@@ -319,6 +325,8 @@ read the man page virt-builder(1).
   (* Dereference options. *)
   let args = List.rev !args in
   let mode = !mode in
+  let arch = !arch in
+  let allow_foreign_arch_ops = !allow_foreign_arch_ops in
   let attach = List.rev !attach in
   let cache = !cache in
   let check_signature = !check_signature in
@@ -431,10 +439,25 @@ read the man page virt-builder(1).
     (* Combine the sources and fingerprints into a single list of pairs. *)
     List.combine sources fingerprints in

+  (* Check the architecture. *)
+  let arch +    match arch with
+    | "" -> Architecture.current_arch
+    | arch ->
+      let target_arch = Architecture.filter_arch arch in
+      if Architecture.current_arch <> target_arch &&
allow_foreign_arch_ops <> true then (
+        if install <> [] || run <> [] || update then (
+          eprintf (f_"%s: sorry, cannot run commands on a guest with a
different architecture\n")
+            prog;
+          exit 1
+        );
+      );
+      target_arch in
+
   mode, arg,
-  attach, cache, check_signature, curl, debug, delete, delete_on_failure,
-  edit, firstboot, run, format, gpg, hostname, install, list_format, links,
-  memsize, mkdirs,
+  arch, attach, cache, check_signature, curl, debug, delete,
+  delete_on_failure, edit, firstboot, run, format, gpg, hostname, install,
+  list_format, links, memsize, mkdirs,
   network, output, password_crypto, quiet, root_password, scrub,
   scrub_logfile, selinux_relabel, size, smp, sources, sync, timezone,
   update, upload, writes
diff --git a/builder/downloader.ml b/builder/downloader.ml
index e386c06..e23cb37 100644
--- a/builder/downloader.ml
+++ b/builder/downloader.ml
@@ -25,8 +25,8 @@ open Printf
 let quote = Filename.quote
 let (//) = Filename.concat

-let cache_of_name cachedir name revision -  cachedir // sprintf
"%s.%d" name revision
+let cache_of_name cachedir name arch revision +  cachedir // sprintf
"%s.%s.%d" name arch revision

 type uri = string
 type filename = string
@@ -51,14 +51,14 @@ let rec download ~prog t ?template ?progress_bar uri
download_to ~prog t ?progress_bar uri tmpfile;
     (tmpfile, true)

-  | Some (name, revision) ->
+  | Some (name, arch, revision) ->
     match t.cache with
     | None ->
       (* Not using the cache at all? *)
       download t ~prog ?progress_bar uri

     | Some cachedir ->
-      let filename = cache_of_name cachedir name revision in
+      let filename = cache_of_name cachedir name arch revision in

       (* Is the requested template name + revision in the cache already?
        * If not, download it.
diff --git a/builder/downloader.mli b/builder/downloader.mli
index 62d500d..7b26ba3 100644
--- a/builder/downloader.mli
+++ b/builder/downloader.mli
@@ -18,7 +18,7 @@

 (** This module is a wrapper around curl, plus local caching. *)

-val cache_of_name : string -> string -> int -> string
+val cache_of_name : string -> string -> string -> int -> string
 (** [cache_of_name cachedir name revision] returns the filename
     of the cached file.  (Note: It doesn't check if the filename
     exists, this is just a simple string transformation). *)
@@ -32,15 +32,15 @@ type t
 val create : debug:bool -> curl:string -> cache:string option -> t
 (** Create the abstract type. *)

-val download : prog:string -> t -> ?template:(string*int) ->
?progress_bar:bool -> uri -> (filename * bool)
+val download : prog:string -> t -> ?template:(string*string*int) ->
?progress_bar:bool -> uri -> (filename * bool)
 (** Download the URI, returning the downloaded filename and a
     temporary file flag.  The temporary file flag is [true] iff
     the downloaded file is temporary and should be deleted by the
     caller (otherwise it's in the cache and you shouldn't delete it).

-    For templates, you must supply [~template:(name, revision)].  This
-    causes the cache to be used (if possible).  Name and revision are
-    used for cache control (see the man page for details).
+    For templates, you must supply [~template:(name, arch, revision)].
+    This causes the cache to be used (if possible).  Name, arch(itecture)
+    and revision are used for cache control (see the man page for details).

     If [~progress_bar:true] then display a progress bar if the file
     doesn't come from the cache.  In debug mode, progress messages
diff --git a/builder/index_parser.ml b/builder/index_parser.ml
index 9fd9cda..472a6c7 100644
--- a/builder/index_parser.ml
+++ b/builder/index_parser.ml
@@ -123,16 +123,26 @@ let get_index ~prog ~debug ~downloader ~sigchecker source
if delete_tmpfile then
       (try Unix.unlink tmpfile with _ -> ());

-    (* Check for repeated os-version names. *)
+    (* Check for repeated os-version+arch combination. *)
+    let name_arch_map = List.map (
+      fun (n, fields) ->
+        let rec find_arch = function
+          | ("arch", None, value) :: y -> value
+          | _ :: y -> find_arch y
+          | [] -> ""
+        in
+        n, (find_arch fields)
+    ) sections in
     let nseen = Hashtbl.create 13 in
     List.iter (
-      fun (n, _) ->
-        if Hashtbl.mem nseen n then (
-          eprintf (f_"virt-builder: index is corrupt: os-version
'%s' appears two or more times\n") n;
+      fun (n, arch) ->
+        let id = n, arch in
+        if Hashtbl.mem nseen id then (
+          eprintf (f_"virt-builder: index is corrupt: os-version
'%s' with architecture '%s' appears two or more times\n") n
arch;
           corrupt_file ()
         );
-        Hashtbl.add nseen n true
-    ) sections;
+        Hashtbl.add nseen id true
+    ) name_arch_map;

     (* Check for repeated fields. *)
     List.iter (
diff --git a/builder/list_entries.ml b/builder/list_entries.ml
index 476bf14..75149e7 100644
--- a/builder/list_entries.ml
+++ b/builder/list_entries.ml
@@ -48,9 +48,11 @@ let rec list_entries ~list_format ~sources index  and
list_entries_short index    List.iter (
     fun (name, { Index_parser.printable_name = printable_name;
+                 arch = arch;
                  hidden = hidden }) ->
       if not hidden then (
         printf "%-24s" name;
+        printf " %-10s" arch;
         (match printable_name with
         | None -> ()
         | Some s -> printf " %s" s
diff --git a/builder/test-virt-builder-list.sh
b/builder/test-virt-builder-list.sh
index 2f9b319..1f62838 100755
--- a/builder/test-virt-builder-list.sh
+++ b/builder/test-virt-builder-list.sh
@@ -27,13 +27,13 @@ export XDG_CONFIG_DIRS="$abs_builddir/test-config"

 short_list=$($VG ./virt-builder --no-check-signature --no-cache --list)

-if [ "$short_list" != "phony-debian             Phony Debian
-phony-fedora             Phony Fedora
-phony-fedora-qcow2       Phony Fedora qcow2
-phony-fedora-qcow2-uncompressed Phony Fedora qcow2 uncompressed
-phony-fedora-no-format   Phony Fedora
-phony-ubuntu             Phony Ubuntu
-phony-windows            Phony Windows" ]; then
+if [ "$short_list" != "phony-debian             x86_64     Phony
Debian
+phony-fedora             x86_64     Phony Fedora
+phony-fedora-qcow2       x86_64     Phony Fedora qcow2
+phony-fedora-qcow2-uncompressed x86_64     Phony Fedora qcow2 uncompressed
+phony-fedora-no-format   x86_64     Phony Fedora
+phony-ubuntu             x86_64     Phony Ubuntu
+phony-windows            x86_64     Phony Windows" ]; then
     echo "$0: unexpected --list output:"
     echo "$short_list"
     exit 1
diff --git a/builder/test-virt-builder.sh b/builder/test-virt-builder.sh
index 85a7888..8841984 100755
--- a/builder/test-virt-builder.sh
+++ b/builder/test-virt-builder.sh
@@ -52,6 +52,7 @@ rm -f $output
 $VG ./virt-builder phony-fedora \
     -v --no-cache --no-check-signature $no_network \
     -o $output --size 2G --format $format \
+    --arch x86_64 --allow-foreign-arch-ops \
     --hostname test.example.com \
     --timezone Europe/London \
     --root-password password:123456 \
diff --git a/builder/uname-c.c b/builder/uname-c.c
new file mode 100644
index 0000000..b4f2f61
--- /dev/null
+++ b/builder/uname-c.c
@@ -0,0 +1,60 @@
+/* virt-builder
+ * Copyright (C) 2014 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA.
+ */
+
+/* This file handles the interface between the C/lex/yacc index file
+ * parser, and the OCaml world.  See index_parser.ml for the OCaml
+ * type definition.
+ */
+
+#include <config.h>
+
+#include <errno.h>
+#include <sys/utsname.h>
+
+#include <caml/alloc.h>
+#include <caml/fail.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+
+#ifdef HAVE_CAML_UNIXSUPPORT_H
+#include <caml/unixsupport.h>
+#else
+#define Nothing ((value) 0)
+extern void unix_error (int errcode, char * cmdname, value arg) Noreturn;
+#endif
+
+value
+virt_builder_uname (value unit)
+{
+  CAMLparam0 ();
+  CAMLlocal1 (rv);
+  struct utsname u;
+
+  if (uname (&u) < 0)
+    unix_error (errno, (char *) "uname", Val_int (0));
+
+  rv = caml_alloc (5, 0);
+
+  Store_field (rv, 0, caml_copy_string (u.sysname));
+  Store_field (rv, 1, caml_copy_string (u.nodename));
+  Store_field (rv, 2, caml_copy_string (u.release));
+  Store_field (rv, 3, caml_copy_string (u.version));
+  Store_field (rv, 4, caml_copy_string (u.machine));
+
+  CAMLreturn (rv);
+}
diff --git a/builder/uname.ml b/builder/uname.ml
new file mode 100644
index 0000000..c370c2c
--- /dev/null
+++ b/builder/uname.ml
@@ -0,0 +1,27 @@
+(* virt-builder
+ * Copyright (C) 2014 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+type uname_struct = {
+  sysname : string;
+  nodename : string;
+  release : string;
+  version : string;
+  machine : string;
+}
+
+external uname : unit -> uname_struct = "virt_builder_uname"
diff --git a/builder/uname.mli b/builder/uname.mli
new file mode 100644
index 0000000..aea441b
--- /dev/null
+++ b/builder/uname.mli
@@ -0,0 +1,28 @@
+(* virt-builder
+ * Copyright (C) 2014 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+type uname_struct = {
+  sysname : string;
+  nodename : string;
+  release : string;
+  version : string;
+  machine : string;
+}
+
+val uname : unit -> uname_struct
+(** [uname] Tiny wrapper to the C [uname]. *)
diff --git a/builder/virt-builder.pod b/builder/virt-builder.pod
index 32c0961..708bfd0 100644
--- a/builder/virt-builder.pod
+++ b/builder/virt-builder.pod
@@ -15,6 +15,8 @@ virt-builder - Build virtual machine images quickly

  virt-builder os-version
     [-o|--output DISKIMAGE] [--size SIZE] [--format raw|qcow2]
+    [--allow-foreign-arch-ops]
+    [--arch ARCHITECTURE]
     [--attach ISOFILE]
     [--root-password SELECTOR]
     [--hostname HOSTNAME]
@@ -81,7 +83,9 @@ are any installation notes:

  virt-builder fedora-20

-will build a Fedora 20 image.  This will have all default
+will build a Fedora 20 image for the same architecture as virt-builder
+(so running it from an i386 installation will try to build an i386
+image, if available).  This will have all default
 configuration (minimal size, no user accounts, random root password,
 only the bare minimum installed software, etc.).

@@ -109,6 +113,10 @@ As above, but the output size will be 20 GB.  The guest OS
is resized
 as it is copied to the output (automatically, using
 L<virt-resize(1)>).

+ virt-builder fedora-20 --arch i386
+
+As above, but using an i386 template, if available.
+
 =head2 Setting the root password

  virt-builder fedora-20 --root-password file:/tmp/rootpw
@@ -189,6 +197,21 @@ You can combine these options, and have multiple options of
all types.

 Display help.

+=item B<--allow-foreign-arch-ops>
+
+Forces the execution of commands on the target image even if its
+architecture is different than the current host ones.
+
+See also L</ARCHITECTURE>.
+
+=item B<--arch> ARCHITECTURE
+
+Use the specified architecture for the output image.  This means
+there must be sources providing the requested template for the
+requested architecture.
+
+See also L</ARCHITECTURE>.
+
 =item B<--attach> ISOFILE

 During the customization phase, the given disk is attached to the
@@ -1635,13 +1658,13 @@ highly recommended that you always create signed index
and templates.
 Virt-builder can build a guest for any architecture no matter what the
 host architecture is.  For example an x86-64 guest on an ARM host.

-However certain options may not work correctly, specifically options
+However certain options may not work, specifically options
 that require running commands in the guest during the build process:
 I<--install>, I<--update>, I<--run>, I<--run-command>.
You may need
-to replace these with their firstboot-equivalents.
-
-An x86-64 host building 32 bit i686 guests should work without any
-special steps.
+to replace these with their firstboot-equivalents, or specify
+I<--allow-foreign-arch-ops> if the target architecture is compatible
+with the host architecture (for example, you are building a 32 bit
+i686 guest on a x86-64 host).

 =head2 SECURITY

diff --git a/po/POTFILES b/po/POTFILES
index 5b2ec57..5aeeb2e 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -6,6 +6,7 @@ builder/index-struct.c
 builder/index-validate.c
 builder/pxzcat-c.c
 builder/setlocale-c.c
+builder/uname-c.c
 cat/cat.c
 cat/filesystems.c
 cat/ls.c
diff --git a/po/POTFILES-ml b/po/POTFILES-ml
index 8725e5e..c96b451 100644
--- a/po/POTFILES-ml
+++ b/po/POTFILES-ml
@@ -1,3 +1,4 @@
+builder/architecture.ml
 builder/builder.ml
 builder/cmdline.ml
 builder/downloader.ml
@@ -10,6 +11,7 @@ builder/pxzcat.ml
 builder/setlocale.ml
 builder/sigchecker.ml
 builder/sources.ml
+builder/uname.ml
 mllib/common_gettext.ml
 mllib/common_utils.ml
 mllib/common_utils_tests.ml
--
1.8.3.1