[Pkg-libvirt-commits] [libguestfs] 19/61: customize: Move virt-customize-related code to a separate directory.

Hilko Bengen bengen at moszumanska.debian.org
Sat Mar 29 14:36:23 UTC 2014


This is an automated email from the git hooks/post-receive script.

bengen pushed a commit to branch experimental
in repository libguestfs.

commit 4b0b3589e854f0ed9174250490dc9c6ade94292f
Author: Richard W.M. Jones <rjones at redhat.com>
Date:   Mon Mar 17 15:34:31 2014 +0000

    customize: Move virt-customize-related code to a separate directory.
    
    Split virt-builder into build and customize steps, so that we can spin
    off a separate tool called 'virt-customize'.  This commit does not in
    fact create such a tool, but it moves all the common code into a
    library, in the customize/ subdirectory of the source.
    
    Although this is mostly refactoring, it does change the order in which
    virt-builder command line arguments are processed, so they are now
    processed in the order they appear, not the inflexible fixed order
    used before.
---
 .gitignore                                        |   6 +
 Makefile.am                                       |  12 +-
 builder/Makefile.am                               |  33 +-
 builder/builder.ml                                | 333 +-----------
 builder/cmdline.ml                                | 230 ++------
 builder/virt-builder.pod                          | 262 +--------
 configure.ac                                      |   1 +
 {mllib => customize}/Makefile.am                  | 137 ++---
 {mllib => customize}/crypt-c.c                    |   0
 {mllib => customize}/crypt.ml                     |   0
 {mllib => customize}/crypt.mli                    |   0
 customize/customize_run.ml                        | 328 +++++++++++
 mllib/timezone.mli => customize/customize_run.mli |  14 +-
 {mllib => customize}/firstboot.ml                 |   0
 {mllib => customize}/firstboot.mli                |   0
 {mllib => customize}/hostname.ml                  |   0
 {mllib => customize}/hostname.mli                 |   0
 {mllib => customize}/password.ml                  |   0
 {mllib => customize}/password.mli                 |   0
 {mllib => customize}/perl_edit.ml                 |   0
 {mllib => customize}/perl_edit.mli                |   0
 {mllib => customize}/random_seed.ml               |   0
 {mllib => customize}/random_seed.mli              |   0
 {mllib => customize}/timezone.ml                  |   0
 {mllib => customize}/timezone.mli                 |   0
 {mllib => customize}/urandom.ml                   |   0
 {mllib => customize}/urandom.mli                  |   0
 generator/Makefile.am                             |   2 +
 generator/customize.ml                            | 634 ++++++++++++++++++++++
 generator/main.ml                                 |   6 +
 mllib/Makefile.am                                 |  26 -
 po-docs/ja/Makefile.am                            |   9 +
 po-docs/uk/Makefile.am                            |   9 +
 po/POTFILES                                       |   2 +-
 po/POTFILES-ml                                    |   8 -
 src/guestfs.pod                                   |   4 +
 sysprep/Makefile.am                               |  23 +-
 37 files changed, 1164 insertions(+), 915 deletions(-)

diff --git a/.gitignore b/.gitignore
index 317ddd5..a93eb95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,6 +90,12 @@ Makefile.in
 /config.sub
 /configure
 /csharp/Libguestfs.cs
+/customize/.depend
+/customize/customize_cmdline.ml
+/customize/customize_cmdline.mli
+/customize/customize-options.pod
+/customize/customize-synopsis.pod
+/customize/virt-customize
 /daemon/actions.h
 /daemon/errnostring.c
 /daemon/errnostring-gperf.c
diff --git a/Makefile.am b/Makefile.am
index aa176db..5b8a82e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -123,10 +123,16 @@ endif
 # Unconditional because nothing is built yet.
 SUBDIRS += csharp
 
-# OCaml tools.  Note 'mllib' contains random shared code used by
-# all of the OCaml tools.
+# OCaml tools.  Note 'mllib' and 'customize' contain shared code used
+# by other OCaml tools, so these must come first.
 if HAVE_OCAML
-SUBDIRS += mllib builder builder/website resize sparsify sysprep
+SUBDIRS += \
+	mllib \
+	customize \
+	builder builder/website \
+	resize \
+	sparsify \
+	sysprep
 endif
 
 # Perl tools.
diff --git a/builder/Makefile.am b/builder/Makefile.am
index ad791e9..a777942 100644
--- a/builder/Makefile.am
+++ b/builder/Makefile.am
@@ -77,26 +77,28 @@ if HAVE_OCAML
 # Note this list must be in dependency order.
 deps = \
 	$(top_builddir)/mllib/libdir.cmx \
+	$(top_builddir)/mllib/config.cmx \
 	$(top_builddir)/mllib/common_gettext.cmx \
 	$(top_builddir)/mllib/common_utils.cmx \
-	$(top_builddir)/mllib/urandom.cmx \
-	$(top_builddir)/mllib/random_seed.cmx \
-	$(top_builddir)/mllib/hostname.cmx \
-	$(top_builddir)/mllib/timezone.cmx \
-	$(top_builddir)/mllib/firstboot.cmx \
-	$(top_builddir)/mllib/perl_edit.cmx \
-	$(top_builddir)/mllib/crypt-c.o \
-	$(top_builddir)/mllib/crypt.cmx \
 	$(top_builddir)/mllib/fsync-c.o \
 	$(top_builddir)/mllib/fsync.cmx \
-	$(top_builddir)/mllib/password.cmx \
 	$(top_builddir)/mllib/planner.cmx \
-	$(top_builddir)/mllib/config.cmx \
-	$(top_builddir)/fish/guestfish-uri.o \
 	$(top_builddir)/mllib/uri-c.o \
 	$(top_builddir)/mllib/uRI.cmx \
 	$(top_builddir)/mllib/mkdtemp-c.o \
 	$(top_builddir)/mllib/mkdtemp.cmx \
+	$(top_builddir)/customize/urandom.cmx \
+	$(top_builddir)/customize/random_seed.cmx \
+	$(top_builddir)/customize/hostname.cmx \
+	$(top_builddir)/customize/timezone.cmx \
+	$(top_builddir)/customize/firstboot.cmx \
+	$(top_builddir)/customize/perl_edit.cmx \
+	$(top_builddir)/customize/crypt-c.o \
+	$(top_builddir)/customize/crypt.cmx \
+	$(top_builddir)/customize/password.cmx \
+	$(top_builddir)/customize/customize_cmdline.cmx \
+	$(top_builddir)/customize/customize_run.cmx \
+	$(top_builddir)/fish/guestfish-uri.o \
 	index-scan.o \
 	index-struct.o \
 	index-parse.o \
@@ -135,7 +137,8 @@ OCAMLPACKAGES = \
 	-package str,unix \
 	-I $(top_builddir)/src/.libs \
 	-I $(top_builddir)/ocaml \
-	-I $(top_builddir)/mllib
+	-I $(top_builddir)/mllib \
+	-I $(top_builddir)/customize
 if HAVE_OCAML_PKG_GETTEXT
 OCAMLPACKAGES += -package gettext-stub
 endif
@@ -182,10 +185,12 @@ noinst_DATA += $(top_builddir)/html/virt-builder.1.html
 
 virt-builder.1 $(top_builddir)/html/virt-builder.1.html: stamp-virt-builder.pod
 
-stamp-virt-builder.pod: virt-builder.pod
+stamp-virt-builder.pod: virt-builder.pod $(top_srcdir)/customize/customize-synopsis.pod $(top_srcdir)/customize/customize-options.pod
 	$(PODWRAPPER) \
 	  --man virt-builder.1 \
 	  --html $(top_builddir)/html/virt-builder.1.html \
+	  --insert $(top_srcdir)/customize/customize-synopsis.pod:__CUSTOMIZE_SYNOPSIS__ \
+	  --insert $(top_srcdir)/customize/customize-options.pod:__CUSTOMIZE_OPTIONS__ \
 	  --license GPLv2+ \
 	  $<
 	touch $@
@@ -236,7 +241,7 @@ depend: .depend
 
 .depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml)
 	rm -f $@ $@-t
-	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib $^ | \
+	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib -I $(abs_top_builddir)/customize $^ | \
 	  $(SED) 's/ *$$//' | \
 	  $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \
 	  $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \
diff --git a/builder/builder.ml b/builder/builder.ml
index b3ca46a..81eb2d9 100644
--- a/builder/builder.ml
+++ b/builder/builder.ml
@@ -25,6 +25,7 @@ open Password
 open Planner
 
 open Cmdline
+open Customize_cmdline
 
 open Unix
 open Printf
@@ -38,12 +39,9 @@ let () = Random.self_init ()
 let main () =
   (* Command line argument parsing - see cmdline.ml. *)
   let mode, arg,
-    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 =
+    arch, attach, cache, check_signature, curl, debug,
+    delete_on_failure, format, gpg, list_format, memsize,
+    network, ops, output, quiet, size, smp, sources, sync =
     parse_cmdline () in
 
   (* Timestamped messages in ordinary, non-debug non-quiet mode. *)
@@ -593,7 +591,7 @@ let main () =
     (match smp with None -> () | Some smp -> g#set_smp smp);
     g#set_network network;
 
-    g#set_selinux selinux_relabel;
+    g#set_selinux ops.flags.selinux_relabel;
 
     (* The output disk is being created, so use cache=unsafe here. *)
     g#add_drive_opts ~format:output_format ~cachemode:"unsafe" output_filename;
@@ -626,313 +624,7 @@ let main () =
       eprintf (f_"%s: no guest operating systems or multiboot OS found in this disk image\nThis is a failure of the source repository.  Use -v for more information.\n") prog;
       exit 1 in
 
-  (* Set the random seed. *)
-  msg (f_"Setting a random seed");
-  if not (Random_seed.set_random_seed g root) then
-    eprintf (f_"%s: warning: random seed could not be set for this type of guest\n%!") prog;
-
-  (* Set the hostname. *)
-  (match hostname with
-  | None -> ()
-  | Some hostname ->
-    msg (f_"Setting the hostname: %s") hostname;
-    if not (Hostname.set_hostname g root hostname) then
-      eprintf (f_"%s: warning: hostname could not be set for this type of guest\n%!") prog
-  );
-
-  (* Set the timezone. *)
-  (match timezone with
-  | None -> ()
-  | Some timezone ->
-    msg (f_"Setting the timezone: %s") timezone;
-    if not (Timezone.set_timezone ~prog g root timezone) then
-      eprintf (f_"%s: warning: timezone could not be set for this type of guest\n%!") prog
-  );
-
-  (* Root password.
-   * Note 'None' means that we randomize the root password.
-   *)
-  let () =
-    match g#inspect_get_type root with
-    | "linux" ->
-      let password_map = Hashtbl.create 1 in
-      let pw =
-        match root_password with
-        | Some pw ->
-          msg (f_"Setting root password");
-          pw
-        | None ->
-          msg (f_"Setting random root password [did you mean to use --root-password?]");
-          parse_selector ~prog "random" in
-      Hashtbl.replace password_map "root" pw;
-      set_linux_passwords ~prog ?password_crypto g root password_map
-    | _ ->
-      eprintf (f_"%s: warning: root password could not be set for this type of guest\n%!") prog in
-
-  (* Based on the guest type, choose a log file location. *)
-  let logfile =
-    match g#inspect_get_type root with
-    | "windows" | "dos" ->
-      if g#is_dir ~followsymlinks:true "/Temp" then "/Temp/builder.log"
-      else "/builder.log"
-    | _ ->
-      if g#is_dir ~followsymlinks:true "/tmp" then "/tmp/builder.log"
-      else "/builder.log" in
-
-  (* Function to cat the log file, for debugging and error messages. *)
-  let debug_logfile () =
-    try
-      (* XXX If stderr is redirected this actually truncates the
-       * redirection file, which is pretty annoying to say the
-       * least.
-       *)
-      g#download logfile "/dev/stderr"
-    with exn ->
-      eprintf (f_"%s: log file %s: %s (ignored)\n")
-        prog logfile (Printexc.to_string exn) in
-
-  (* Useful wrapper for scripts. *)
-  let do_run ~display cmd =
-    (* Add a prologue to the scripts:
-     * - Pass environment variables through from the host.
-     * - Send stdout and stderr to a log file so we capture all output
-     *   in error messages.
-     * Also catch errors and dump the log file completely on error.
-     *)
-    let env_vars =
-      filter_map (
-        fun name ->
-          try Some (sprintf "export %s=%s" name (quote (Sys.getenv name)))
-          with Not_found -> None
-      ) [ "http_proxy"; "https_proxy"; "ftp_proxy"; "no_proxy" ] in
-    let env_vars = String.concat "\n" env_vars ^ "\n" in
-
-    let cmd = sprintf "\
-exec >>%s 2>&1
-%s
-%s
-" (quote logfile) env_vars cmd in
-
-    if debug then eprintf "running command:\n%s\n%!" cmd;
-    try ignore (g#sh cmd)
-    with
-      Guestfs.Error msg ->
-        debug_logfile ();
-        eprintf (f_"%s: %s: command exited with an error\n") prog display;
-        exit 1
-  in
-
-  (* http://distrowatch.com/dwres.php?resource=package-management *)
-  let guest_install_command packages =
-    let quoted_args = String.concat " " (List.map quote packages) in
-    match g#inspect_get_package_management root with
-    | "apt" ->
-      (* http://unix.stackexchange.com/questions/22820 *)
-      sprintf "
-        export DEBIAN_FRONTEND=noninteractive
-        apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
-        apt-get $apt_opts update
-        apt-get $apt_opts install %s
-      " quoted_args
-    | "pisi" ->
-      sprintf "pisi it %s" quoted_args
-    | "pacman" ->
-      sprintf "pacman -S %s" quoted_args
-    | "urpmi" ->
-      sprintf "urpmi %s" quoted_args
-    | "yum" ->
-      sprintf "yum -y install %s" quoted_args
-    | "zypper" ->
-      (* XXX Should we use -n option? *)
-      sprintf "zypper in %s" quoted_args
-    | "unknown" ->
-      eprintf (f_"%s: --install is not supported for this guest operating system\n")
-        prog;
-      exit 1
-    | pm ->
-      eprintf (f_"%s: sorry, don't know how to use --install with the '%s' package manager\n")
-        prog pm;
-      exit 1
-
-  and guest_update_command () =
-    match g#inspect_get_package_management root with
-    | "apt" ->
-      (* http://unix.stackexchange.com/questions/22820 *)
-      sprintf "
-        export DEBIAN_FRONTEND=noninteractive
-        apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
-        apt-get $apt_opts update
-        apt-get $apt_opts upgrade
-      "
-    | "pisi" ->
-      sprintf "pisi upgrade"
-    | "pacman" ->
-      sprintf "pacman -Su"
-    | "urpmi" ->
-      sprintf "urpmi --auto-select"
-    | "yum" ->
-      sprintf "yum -y update"
-    | "zypper" ->
-      sprintf "zypper update"
-    | "unknown" ->
-      eprintf (f_"%s: --update is not supported for this guest operating system\n")
-        prog;
-      exit 1
-    | pm ->
-      eprintf (f_"%s: sorry, don't know how to use --update with the '%s' package manager\n")
-        prog pm;
-      exit 1
-  in
-
-  (* Update core/template packages. *)
-  if update then (
-    msg (f_"Updating core packages");
-
-    let cmd = guest_update_command () in
-    do_run ~display:cmd cmd
-  );
-
-  (* Install packages. *)
-  if install <> [] then (
-    msg (f_"Installing packages: %s") (String.concat " " install);
-
-    let cmd = guest_install_command install in
-    do_run ~display:cmd cmd
-  );
-
-  (* Make directories. *)
-  List.iter (
-    fun dir ->
-      msg (f_"Making directory: %s") dir;
-      g#mkdir_p dir
-  ) mkdirs;
-
-  (* Write files. *)
-  List.iter (
-    fun (file, content) ->
-      msg (f_"Writing: %s") file;
-      g#write file content
-  ) writes;
-
-  (* Upload files. *)
-  List.iter (
-    fun (file, dest) ->
-      msg (f_"Uploading: %s to %s") file dest;
-      let dest =
-        if g#is_dir ~followsymlinks:true dest then
-          dest ^ "/" ^ Filename.basename file
-        else
-          dest in
-      (* Do the file upload. *)
-      g#upload file dest;
-
-      (* Copy (some of) the permissions from the local file to the
-       * uploaded file.
-       *)
-      let statbuf = stat file in
-      let perms = statbuf.st_perm land 0o7777 (* sticky & set*id *) in
-      g#chmod perms dest;
-      let uid, gid = statbuf.st_uid, statbuf.st_gid in
-      g#chown uid gid dest
-  ) upload;
-
-  (* Edit files. *)
-  List.iter (
-    fun (file, expr) ->
-      msg (f_"Editing: %s") file;
-
-      if not (g#is_file file) then (
-        eprintf (f_"%s: error: %s is not a regular file in the guest\n")
-          prog file;
-        exit 1
-      );
-
-      Perl_edit.edit_file ~debug g file expr
-  ) edit;
-
-  (* Delete files. *)
-  List.iter (
-    fun file ->
-      msg (f_"Deleting: %s") file;
-      g#rm_rf file
-  ) delete;
-
-  (* Symbolic links. *)
-  List.iter (
-    fun (target, links) ->
-      List.iter (
-        fun link ->
-          msg (f_"Linking: %s -> %s") link target;
-          g#ln_sf target link
-      ) links
-  ) links;
-
-  (* Scrub files. *)
-  List.iter (
-    fun file ->
-      msg (f_"Scrubbing: %s") file;
-      g#scrub_file file
-  ) scrub;
-
-  (* Firstboot scripts/commands/install. *)
-  let () =
-    let i = ref 0 in
-    List.iter (
-      fun op ->
-        incr i;
-        match op with
-        | `Script script ->
-          msg (f_"Installing firstboot script: [%d] %s") !i script;
-          let cmd = read_whole_file script in
-          Firstboot.add_firstboot_script g root !i cmd
-        | `Command cmd ->
-          msg (f_"Installing firstboot command: [%d] %s") !i cmd;
-          Firstboot.add_firstboot_script g root !i cmd
-        | `Packages pkgs ->
-          msg (f_"Installing firstboot packages: [%d] %s") !i
-            (String.concat " " pkgs);
-          let cmd = guest_install_command pkgs in
-          Firstboot.add_firstboot_script g root !i cmd
-    ) firstboot in
-
-  (* Run scripts. *)
-  List.iter (
-    function
-    | `Script script ->
-      msg (f_"Running: %s") script;
-      let cmd = read_whole_file script in
-      do_run ~display:script cmd
-    | `Command cmd ->
-      msg (f_"Running: %s") cmd;
-      do_run ~display:cmd cmd
-  ) run;
-
-  if selinux_relabel then (
-    msg (f_"SELinux relabelling");
-    let cmd = sprintf "
-      if load_policy && fixfiles restore; then
-        rm -f /.autorelabel
-      else
-        touch /.autorelabel
-        echo '%s: SELinux relabelling failed, will relabel at boot instead.'
-      fi
-    " prog in
-    do_run ~display:"load_policy && fixfiles restore" cmd
-  );
-
-  (* Clean up the log file:
-   *
-   * If debugging, dump out the log file.
-   * Then if asked, scrub the log file.
-   *)
-  if debug then debug_logfile ();
-  if scrub_logfile && g#exists logfile then (
-    msg (f_"Scrubbing the log file");
-
-    (* Try various methods with decreasing complexity. *)
-    try g#scrub_file logfile
-    with _ -> g#rm_f logfile
-  );
+  Customize_run.run ~prog ~debug ~quiet g root ops;
 
   (* Collect some stats about the final output file.
    * Notes:
@@ -976,19 +668,6 @@ exec >>%s 2>&1
   (* Unmount everything and we're done! *)
   msg (f_"Finishing off");
 
-  (* Kill any daemons (eg. started by newly installed packages) using
-   * the sysroot.
-   * XXX How to make this nicer?
-   * XXX fuser returns an error if it doesn't kill any processes, which
-   * is not very useful.
-   *)
-  (try ignore (g#debug "sh" [| "fuser"; "-k"; "/sysroot" |])
-   with exn ->
-     if debug then
-       eprintf (f_"%s: %s (ignored)\n") prog (Printexc.to_string exn)
-  );
-  g#ping_daemon (); (* tiny delay after kill *)
-
   g#umount_all ();
   g#shutdown ();
   g#close ();
diff --git a/builder/cmdline.ml b/builder/cmdline.ml
index 2657906..7cd3342 100644
--- a/builder/cmdline.ml
+++ b/builder/cmdline.ml
@@ -21,9 +21,9 @@
 open Common_gettext.Gettext
 open Common_utils
 
-module G = Guestfs
+open Customize_cmdline
 
-open Password
+module G = Guestfs
 
 open Unix
 open Printf
@@ -62,67 +62,14 @@ let parse_cmdline () =
   let curl = ref "curl" in
   let debug = ref false in
 
-  let delete = ref [] in
-  let add_delete s = delete := s :: !delete in
-
   let delete_on_failure = ref true in
 
-  let edit = ref [] in
-  let add_edit arg =
-    let i =
-      try String.index arg ':'
-      with Not_found ->
-        eprintf (f_"%s: invalid --edit format, see the man page.\n") prog;
-        exit 1 in
-    let len = String.length arg in
-    let file = String.sub arg 0 i in
-    let expr = String.sub arg (i+1) (len-(i+1)) in
-    edit := (file, expr) :: !edit
-  in
-
   let fingerprints = ref [] in
   let add_fingerprint arg = fingerprints := arg :: !fingerprints in
 
-  let firstboot = ref [] in
-  let add_firstboot s =
-    if not (Sys.file_exists s) then (
-      if not (String.contains s ' ') then
-        eprintf (f_"%s: %s: %s: file not found\n") prog "--firstboot" s
-      else
-        eprintf (f_"%s: %s: %s: file not found [did you mean %s?]\n") prog "--firstboot" s "--firstboot-command";
-      exit 1
-    );
-    firstboot := `Script s :: !firstboot
-  in
-  let add_firstboot_cmd s = firstboot := `Command s :: !firstboot in
-  let add_firstboot_install pkgs =
-    let pkgs = string_nsplit "," pkgs in
-    firstboot := `Packages pkgs :: !firstboot
-  in
-
   let format = ref "" in
   let gpg = ref "gpg" in
 
-  let hostname = ref None in
-  let set_hostname s = hostname := Some s in
-
-  let install = ref [] in
-  let add_install pkgs =
-    let pkgs = string_nsplit "," pkgs in
-    install := pkgs @ !install
-  in
-
-  let links = ref [] in
-  let add_link arg =
-    let target, lns =
-      match string_nsplit ":" arg with
-      | [] | [_] ->
-        eprintf (f_"%s: invalid --link format, see the man page.\n") prog;
-        exit 1
-      | target :: lns -> target, lns in
-    links := (target, lns) :: !links
-  in
-
   let list_format = ref `Short in
   let list_set_long () = list_format := `Long in
   let list_set_format arg =
@@ -137,44 +84,11 @@ let parse_cmdline () =
   let memsize = ref None in
   let set_memsize arg = memsize := Some arg in
 
-  let mkdirs = ref [] in
-  let add_mkdir arg = mkdirs := arg :: !mkdirs in
-
   let network = ref true in
   let output = ref "" in
 
-  let password_crypto : password_crypto option ref = ref None in
-  let set_password_crypto arg =
-    password_crypto := Some (password_crypto_of_string ~prog arg)
-  in
-
   let quiet = ref false in
 
-  let root_password = ref None in
-  let set_root_password arg =
-    let pw = parse_selector ~prog arg in
-    root_password := Some pw
-  in
-
-  let run = ref [] in
-  let add_run s =
-    if not (Sys.file_exists s) then (
-      if not (String.contains s ' ') then
-        eprintf (f_"%s: %s: %s: file not found\n") prog "--run" s
-      else
-        eprintf (f_"%s: %s: %s: file not found [did you mean %s?]\n") prog "--run" s "--run-command";
-      exit 1
-    );
-    run := `Script s :: !run
-  in
-  let add_run_cmd s = run := `Command s :: !run in
-
-  let scrub = ref [] in
-  let add_scrub s = scrub := s :: !scrub in
-
-  let scrub_logfile = ref false in
-  let selinux_relabel = ref false in
-
   let size = ref None in
   let set_size arg = size := Some (parse_size ~prog arg) in
 
@@ -186,43 +100,7 @@ let parse_cmdline () =
 
   let sync = ref true in
 
-  let timezone = ref None in
-  let set_timezone s = timezone := Some s in
-
-  let update = ref false in
-
-  let upload = ref [] in
-  let add_upload arg =
-    let i =
-      try String.index arg ':'
-      with Not_found ->
-        eprintf (f_"%s: invalid --upload format, see the man page.\n") prog;
-        exit 1 in
-    let len = String.length arg in
-    let file = String.sub arg 0 i in
-    if not (Sys.file_exists file) then (
-      eprintf (f_"%s: --upload: %s: file not found\n") prog file;
-      exit 1
-    );
-    let dest = String.sub arg (i+1) (len-(i+1)) in
-    upload := (file, dest) :: !upload
-  in
-
-  let writes = ref [] in
-  let add_write arg =
-    let i =
-      try String.index arg ':'
-      with Not_found ->
-        eprintf (f_"%s: invalid --write format, see the man page.\n") prog;
-        exit 1 in
-    let len = String.length arg in
-    let file = String.sub arg 0 i in
-    let content = String.sub arg (i+1) (len-(i+1)) in
-    writes := (file, content) :: !writes
-  in
-
-  let ditto = " -\"-" in
-  let argspec = Arg.align [
+  let argspec = [
     "--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,
@@ -233,70 +111,60 @@ let parse_cmdline () =
                                             " " ^ s_"Download all templates to the cache";
     "--check-signature", Arg.Set check_signature,
                                             " " ^ s_"Check digital signatures";
-    "--check-signatures", Arg.Set check_signature, ditto;
+    "--check-signatures", Arg.Set check_signature,
+                                            " " ^ s_"Check digital signatures";
     "--no-check-signature", Arg.Clear check_signature,
                                             " " ^ s_"Disable digital signatures";
-    "--no-check-signatures", Arg.Clear check_signature, ditto;
+    "--no-check-signatures", Arg.Clear check_signature,
+                                            " " ^ s_"Disable digital signatures";
     "--curl",    Arg.Set_string curl,       "curl" ^ " " ^ s_"Set curl binary/command";
-    "--delete",  Arg.String add_delete,     "name" ^ " " ^ s_"Delete a file or dir";
     "--delete-cache", Arg.Unit delete_cache_mode,
                                             " " ^ s_"Delete the template cache";
     "--no-delete-on-failure", Arg.Clear delete_on_failure,
                                             " " ^ s_"Don't delete output file on failure";
-    "--edit",    Arg.String add_edit,       "file:expr" ^ " " ^ s_"Edit file with Perl expr";
     "--fingerprint", Arg.String add_fingerprint,
                                             "AAAA.." ^ " " ^ s_"Fingerprint of valid signing key";
-    "--firstboot", Arg.String add_firstboot, "script" ^ " " ^ s_"Run script at first guest boot";
-    "--firstboot-command", Arg.String add_firstboot_cmd, "cmd+args" ^ " " ^ s_"Run command at first guest boot";
-    "--firstboot-install", Arg.String add_firstboot_install,
-                                            "pkg,pkg" ^ " " ^ s_"Add package(s) to install at firstboot";
     "--format",  Arg.Set_string format,     "raw|qcow2" ^ " " ^ s_"Output format (default: raw)";
     "--get-kernel", Arg.Unit get_kernel_mode,
                                             "image" ^ " " ^ s_"Get kernel from image";
     "--gpg",    Arg.Set_string gpg,         "gpg" ^ " " ^ s_"Set GPG binary/command";
-    "--hostname", Arg.String set_hostname,  "hostname" ^ " " ^ s_"Set the hostname";
-    "--install", Arg.String add_install,    "pkg,pkg" ^ " " ^ s_"Add package(s) to install";
-    "--link",    Arg.String add_link,       "target:link.." ^ " " ^ s_"Create symbolic links";
     "-l",        Arg.Unit list_mode,        " " ^ s_"List available templates";
-    "--list",    Arg.Unit list_mode,        ditto;
+    "--list",    Arg.Unit list_mode,        " " ^ s_"List available templates";
     "--long",    Arg.Unit list_set_long,    " " ^ s_"Shortcut for --list-format short";
     "--list-format", Arg.String list_set_format,
                                             "short|long|json" ^ " " ^ s_"Set the format for --list (default: short)";
-    "--no-logfile", Arg.Set scrub_logfile,  " " ^ s_"Scrub build log file";
     "--long-options", Arg.Unit display_long_options, " " ^ s_"List long options";
     "-m",        Arg.Int set_memsize,       "mb" ^ " " ^ s_"Set memory size";
-    "--memsize", Arg.Int set_memsize,       "mb" ^ ditto;
-    "--mkdir",   Arg.String add_mkdir,      "dir" ^ " " ^ s_"Create directory";
+    "--memsize", Arg.Int set_memsize,       "mb" ^ " " ^ s_"Set memory size";
     "--network", Arg.Set network,           " " ^ s_"Enable appliance network (default)";
     "--no-network", Arg.Clear network,      " " ^ s_"Disable appliance network";
     "--notes",   Arg.Unit notes_mode,       " " ^ s_"Display installation notes";
     "-o",        Arg.Set_string output,     "file" ^ " " ^ s_"Set output filename";
-    "--output",  Arg.Set_string output,     "file" ^ ditto;
-    "--password-crypto", Arg.String set_password_crypto,
-                                            "md5|sha256|sha512" ^ " " ^ s_"Set password crypto";
+    "--output",  Arg.Set_string output,     "file" ^ " " ^ s_"Set output filename";
     "--print-cache", Arg.Unit print_cache_mode,
                                             " " ^ s_"Print info about template cache";
     "--quiet",   Arg.Set quiet,             " " ^ s_"No progress messages";
-    "--root-password", Arg.String set_root_password,
-                                            "..." ^ " " ^ s_"Set root password";
-    "--run",     Arg.String add_run,        "script" ^ " " ^ s_"Run script in disk image";
-    "--run-command", Arg.String add_run_cmd, "cmd+args" ^ " " ^ s_"Run command in disk image";
-    "--scrub",   Arg.String add_scrub,      "name" ^ " " ^ s_"Scrub a file";
-    "--selinux-relabel", Arg.Set selinux_relabel,
-                                            " " ^ s_"Relabel files with correct SELinux labels";
     "--size",    Arg.String set_size,       "size" ^ " " ^ s_"Set output disk size";
     "--smp",     Arg.Int set_smp,           "vcpus" ^ " " ^ s_"Set number of vCPUs";
     "--source",  Arg.String add_source,     "URL" ^ " " ^ s_"Set source URL";
     "--no-sync", Arg.Clear sync,            " " ^ s_"Do not fsync output file on exit";
-    "--timezone",Arg.String set_timezone,   "timezone" ^ " " ^ s_"Set the default timezone";
-    "--update",  Arg.Set update,            " " ^ s_"Update core packages";
-    "--upload",  Arg.String add_upload,     "file:dest" ^ " " ^ s_"Upload file to dest";
     "-v",        Arg.Set debug,             " " ^ s_"Enable debugging messages";
-    "--verbose", Arg.Set debug,             ditto;
+    "--verbose", Arg.Set debug,             " " ^ s_"Enable debugging messages";
     "-V",        Arg.Unit display_version,  " " ^ s_"Display version and exit";
-    "--version", Arg.Unit display_version,  ditto;
-    "--write",   Arg.String add_write,      "file:content" ^ " " ^ s_"Write file";
+    "--version", Arg.Unit display_version,  " " ^ s_"Display version and exit";
   ] in
+  let customize_argspec, get_customize_ops =
+    Customize_cmdline.argspec ~prog () in
+  let customize_argspec =
+    List.map (fun (spec, _, _) -> spec) customize_argspec in
+  let argspec = argspec @ customize_argspec in
+  let argspec =
+    let cmp (arg1, _, _) (arg2, _, _) =
+      let arg1 = skip_dashes arg1 and arg2 = skip_dashes arg2 in
+      compare (String.lowercase arg1) (String.lowercase arg2)
+    in
+    List.sort cmp argspec in
+  let argspec = Arg.align argspec in
   long_options := argspec;
 
   let args = ref [] in
@@ -328,36 +196,20 @@ read the man page virt-builder(1).
   let check_signature = !check_signature in
   let curl = !curl in
   let debug = !debug in
-  let delete = List.rev !delete in
   let delete_on_failure = !delete_on_failure in
-  let edit = List.rev !edit in
   let fingerprints = List.rev !fingerprints in
-  let firstboot = List.rev !firstboot in
-  let run = List.rev !run in
   let format = match !format with "" -> None | s -> Some s in
   let gpg = !gpg in
-  let hostname = !hostname in
-  let install = List.rev !install in
   let list_format = !list_format in
-  let links = List.rev !links in
   let memsize = !memsize in
-  let mkdirs = List.rev !mkdirs in
   let network = !network in
+  let ops = get_customize_ops () in
   let output = match !output with "" -> None | s -> Some s in
-  let password_crypto = !password_crypto in
   let quiet = !quiet in
-  let root_password = !root_password in
-  let scrub = List.rev !scrub in
-  let scrub_logfile = !scrub_logfile in
-  let selinux_relabel = !selinux_relabel in
   let size = !size in
   let smp = !smp in
   let sources = List.rev !sources in
   let sync = !sync in
-  let timezone = !timezone in
-  let update = !update in
-  let upload = List.rev !upload in
-  let writes = List.rev !writes in
 
   (* Check options. *)
   let arg =
@@ -442,7 +294,15 @@ read the man page virt-builder(1).
     | arch ->
       let target_arch = Architecture.filter_arch arch in
       if Architecture.arch_is_compatible Architecture.current_arch target_arch <> true then (
-        if install <> [] || run <> [] || update then (
+        let requires_execute_on_guest = List.exists (
+          function
+          | `Command _ | `InstallPackages _ | `Script _ | `Update -> true
+          | `Delete _ | `Edit _ | `FirstbootCommand _ | `FirstbootPackages _
+          | `FirstbootScript _ | `Hostname _ | `Link _ | `Mkdir _
+          | `RootPassword _ | `Scrub _ | `Timezone _ | `Upload _
+          | `Write _ -> false
+        ) ops.ops in
+        if requires_execute_on_guest then (
           eprintf (f_"%s: sorry, cannot run commands on a guest with a different architecture\n")
             prog;
           exit 1
@@ -450,10 +310,20 @@ read the man page virt-builder(1).
       );
       target_arch in
 
+  (* If user didn't elect any root password, that means we set a random
+   * root password.
+   *)
+  let ops =
+    let has_set_root_password = List.exists (
+      function `RootPassword _ -> true | _ -> false
+    ) ops.ops in
+    if has_set_root_password then ops
+    else (
+      let pw = Password.parse_selector ~prog "random" in
+      { ops with ops = ops.ops @ [ `RootPassword pw ] }
+    ) in
+
   mode, arg,
-  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
+  arch, attach, cache, check_signature, curl, debug,
+  delete_on_failure, format, gpg, list_format, memsize,
+  network, ops, output, quiet, size, smp, sources, sync
diff --git a/builder/virt-builder.pod b/builder/virt-builder.pod
index 7cf345c..2429f66 100644
--- a/builder/virt-builder.pod
+++ b/builder/virt-builder.pod
@@ -13,23 +13,8 @@ virt-builder - Build virtual machine images quickly
 
  virt-builder os-version
     [-o|--output DISKIMAGE] [--size SIZE] [--format raw|qcow2]
-    [--arch ARCHITECTURE]
-    [--attach ISOFILE]
-    [--root-password SELECTOR]
-    [--hostname HOSTNAME]
-    [--timezone TIMEZONE]
-    [--update]
-    [--install PKG,[PKG...]]
-    [--mkdir DIR]
-    [--write FILE:CONTENT]
-    [--upload FILE:DEST]
-    [--link TARGET:LINK[:LINK]]
-    [--edit FILE:EXPR]
-    [--delete FILE] [--scrub FILE]
-    [--selinux-relabel]
-    [--run SCRIPT] [--run-command 'CMD ARGS ...']
-    [--firstboot SCRIPT] [--firstboot-command 'CMD ARGS ...']
-    [--firstboot-install PKG,[PKG...]]
+    [--arch ARCHITECTURE] [--attach ISOFILE]
+__CUSTOMIZE_SYNOPSIS__
 
  virt-builder -l|--list [--long] [--list-format short|long|json]
 
@@ -261,15 +246,6 @@ curl parameters, for example to disable https certificate checks:
 
  virt-builder --curl "curl --insecure" [...]
 
-=item B<--delete> FILE
-
-=item B<--delete> DIR
-
-Delete a file from the guest.  Or delete a directory (and all its
-contents, recursively).
-
-See also: I<--upload>, I<--scrub>.
-
 =item B<--delete-cache>
 
 Delete the template cache.  See L</CACHING>.
@@ -283,17 +259,6 @@ debug images.
 The default is to delete the output file if virt-builder fails (or,
 for example, some script that it runs fails).
 
-=item B<--edit> FILE:EXPR
-
-Edit C<FILE> using the Perl expression C<EXPR>.
-
-Be careful to properly quote the expression to prevent it from
-being altered by the shell.
-
-Note that this option is only available when Perl 5 is installed.
-
-See L<virt-edit(1)/NON-INTERACTIVE EDITING>.
-
 =item B<--fingerprint> 'AAAA BBBB ...'
 
 Check that the index and templates are signed by the key with the
@@ -305,33 +270,6 @@ URLs, then you can have either no fingerprint, one fingerprint or
 multiple fingerprints.  If you have multiple, then each must
 correspond 1-1 with a source URL.
 
-=item B<--firstboot> SCRIPT
-
-=item B<--firstboot-command> 'CMD ARGS ...'
-
-Install C<SCRIPT> inside the guest, so that when the guest first boots
-up, the script runs (as root, late in the boot process).
-
-The script is automatically chmod +x after installation in the guest.
-
-The alternative version I<--firstboot-command> is the same, but it
-conveniently wraps the command up in a single line script for you.
-
-You can have multiple I<--firstboot> and I<--firstboot-command>
-options.  They run in the same order that they appear on the command
-line.
-
-See also I<--run>.
-
-=item B<--firstboot-install> PKG[,PKG,...]
-
-Install the named packages (a comma-separated list).  These are
-installed when the guest first boots using the guest's package manager
-(eg. apt, yum, etc.) and the guest's network connection.
-
-For an overview on the different ways to install packages, see
-L</INSTALLING PACKAGES>.
-
 =item B<--format> qcow2
 
 =item B<--format> raw
@@ -365,29 +303,6 @@ alternate home directory:
 
  virt-builder --gpg "gpg --homedir /tmp" [...]
 
-=item B<--hostname> HOSTNAME
-
-Set the hostname of the guest to C<HOSTNAME>.  You can use a
-dotted hostname.domainname (FQDN) if you want.
-
-=item B<--install> PKG[,PKG,...]
-
-Install the named packages (a comma-separated list).  These are
-installed during the image build using the guest's package manager
-(eg. apt, yum, etc.) and the host's network connection.
-
-For an overview on the different ways to install packages, see
-L</INSTALLING PACKAGES>.
-
-See also I<--update>.
-
-=item B<--link TARGET:LINK>
-
-=item B<--link TARGET:LINK[:LINK...]>
-
-Create symbolic link(s) in the guest, starting at C<LINK> and
-pointing at C<TARGET>.
-
 =item B<-l>
 
 =item B<--list>
@@ -429,14 +344,6 @@ I<--long> is a shorthand for the C<long> format.
 
 See also: I<--source>, I<--notes>, L</SOURCES OF TEMPLATES>.
 
-=item B<--no-logfile>
-
-Scrub C<builder.log> (log file from build commands) from the image
-after building is complete.  If you don't want to reveal precisely how
-the image was built, use this option.
-
-See also: L</LOG FILE>.
-
 =item B<-m> MB
 
 =item B<--memsize> MB
@@ -449,13 +356,6 @@ The default can be found with this command:
 
  guestfish get-memsize
 
-=item B<--mkdir> DIR
-
-Create a directory in the guest.
-
-This uses S<C<mkdir -p>> so any intermediate directories are created,
-and it also works if the directory already exists.
-
 =item B<--network>
 
 =item B<--no-network>
@@ -539,20 +439,6 @@ volume.
 When used with I<--get-kernel>, this option specifies the output
 directory.
 
-=item B<--password-crypto> password-crypto
-
-Set the password encryption to C<md5>, C<sha256> or C<sha512>.
-
-C<sha256> and C<sha512> require glibc E<ge> 2.7 (check crypt(3) inside
-the guest).
-
-C<md5> will work with relatively old Linux guests (eg. RHEL 3), but
-is not secure against modern attacks.
-
-The default is C<sha512> unless libguestfs detects an old guest that
-didn't have support for SHA-512, in which case it will use C<md5>.
-You can override libguestfs by specifying this option.
-
 =item B<--print-cache>
 
 Print information about the template cache.  See L</CACHING>.
@@ -561,62 +447,6 @@ Print information about the template cache.  See L</CACHING>.
 
 Don't print ordinary progress messages.
 
-=item B<--root-password> SELECTOR
-
-Set the root password.
-
-See L</USERS AND PASSWORDS> below for the format of the C<SELECTOR>
-field, and also how to set up user accounts.
-
-Note if you I<don't> set I<--root-password> then the guest is given
-a I<random> root password.
-
-=item B<--run> SCRIPT
-
-=item B<--run-command> 'CMD ARGS ...'
-
-Run the shell script (or any program) called C<SCRIPT> on the disk
-image.  The script runs virtualized inside a small appliance, chrooted
-into the guest filesystem.
-
-The script is automatically chmod +x.
-
-If libguestfs supports it then a limited network connection is
-available but it only allows outgoing network connections.  You can
-also attach data disks (eg. ISO files) as another way to provide data
-(eg. software packages) to the script without needing a network
-connection (I<--attach>).  You can also upload data files (I<--upload>).
-
-The alternative version I<--run-command> is the same, but it
-conveniently wraps the command up in a single line script for you.
-
-You can have multiple I<--run> and I<--run-command> options.  They run
-in the same order that they appear on the command line.
-
-See also: I<--firstboot>, I<--attach>, I<--upload>.
-
-=item B<--scrub> FILE
-
-Scrub a file from the guest.  This is like I<--delete> except that:
-
-=over 4
-
-=item *
-
-It scrubs the data so a guest could not recover it.
-
-=item *
-
-It cannot delete directories, only regular files.
-
-=back
-
-=item B<--selinux-relabel>
-
-Relabel files in the guest so that they have the correct SELinux label.
-
-You should only use this option for guests which support SELinux.
-
 =item B<--size> SIZE
 
 Select the size of the output disk, where the size can be specified
@@ -649,34 +479,6 @@ Note that you should not point I<--source> to sources that you don't
 trust (unless the source is signed by someone you do trust).  See also
 the I<--no-network> option.
 
-=item B<--timezone> TIMEZONE
-
-Set the default timezone of the guest to C<TIMEZONE>.  Use a location
-string like C<Europe/London>
-
-=item B<--update>
-
-Do the equivalent of C<yum update>, C<apt-get upgrade>, or whatever
-command is required to update the packages already installed in the
-template to their latest versions.
-
-See also I<--install>.
-
-=item B<--upload> FILE:DEST
-
-Upload local file C<FILE> to destination C<DEST> in the disk image.
-File owner and permissions from the original are preserved, so you
-should set them to what you want them to be in the disk image.
-
-C<DEST> could be the final filename.  This can be used to rename
-the file on upload.
-
-If C<DEST> is a directory name (which must already exist in the guest)
-then the file is uploaded into that directory, and it keeps the same
-name as on the local filesystem.
-
-See also: I<--mkdir>, I<--delete>, I<--scrub>.
-
 =item B<-v>
 
 =item B<--verbose>
@@ -692,11 +494,11 @@ your bug report.
 
 Display version number and exit.
 
-=item B<--write> FILE:CONTENT
+=back
 
-Write C<CONTENT> to C<FILE>.
+=head2 Customization options
 
-=back
+__CUSTOMIZE_OPTIONS__
 
 =head1 REFERENCE
 
@@ -996,58 +798,8 @@ A new random seed is generated for the guest.
 
 =item *
 
-The hostname and timezone are set (I<--hostname>, I<--timezone>).
-
-=item *
-
-The root password is changed (I<--root-password>).
-
-=item *
-
-Core packages are updated (I<--update>).
-
-=item *
-
-Packages are installed (I<--install>).
-
-=item *
-
-Directories are created (I<--mkdir>).
-
-=item *
-
-Files are written (I<--write>).
-
-=item *
-
-Files are uploaded (I<--upload>).
-
-=item *
-
-Files are edited (I<--edit>).
-
-=item *
-
-Files are deleted (I<--delete>, I<--scrub>).
-
-=item *
-
-Symbolic links are created (I<--link>).
-
-=item *
-
-Firstboot scripts are installed (I<--firstboot>,
-I<--firstboot-command>, I<--firstboot-install>).
-
-Note that although firstboot scripts are installed at this step, they
-do not run until the guest is booted first time.  Firstboot scripts
-will run in the order they appear on the command line.
-
-=item *
-
-Scripts are run (I<--run>, I<--run-command>).
-
-Scripts run in the order they appear on the command line.
+Guest customization is performed, in the order specified on the
+command line.
 
 =item *
 
diff --git a/configure.ac b/configure.ac
index 86cf440..d646b08 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1613,6 +1613,7 @@ AC_CONFIG_FILES([Makefile
                  builder/website/Makefile
                  cat/Makefile
                  csharp/Makefile
+                 customize/Makefile
                  daemon/Makefile
                  df/Makefile
                  diff/Makefile
diff --git a/mllib/Makefile.am b/customize/Makefile.am
similarity index 64%
copy from mllib/Makefile.am
copy to customize/Makefile.am
index e275213..889fe1c 100644
--- a/mllib/Makefile.am
+++ b/customize/Makefile.am
@@ -1,5 +1,5 @@
-# libguestfs OCaml tools common code
-# Copyright (C) 2011-2014 Red Hat Inc.
+# virt-customize
+# 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
@@ -18,85 +18,65 @@
 include $(top_srcdir)/subdir-rules.mk
 
 EXTRA_DIST = \
-	$(filter-out config.ml,$(SOURCES))
+	$(SOURCES)
 
 CLEANFILES = *~ *.cmi *.cmo *.cmx *.cmxa *.o
 
+generator_built = \
+	customize_cmdline.mli \
+	customize_cmdline.ml \
+	customize-options.pod \
+	customize-synopsis.pod
+
 # Alphabetical order.
 SOURCES = \
-	common_gettext.ml \
-	common_utils.ml \
-	common_utils_tests.ml \
-	config.ml \
-	crypt-c.c \
 	crypt.ml \
 	crypt.mli \
-	firstboot.mli \
+	crypt-c.c \
+	customize_cmdline.ml \
+	customize_cmdline.mli \
+	customize_run.ml \
+	customize_run.mli \
 	firstboot.ml \
-	fsync-c.c \
-	fsync.mli \
-	fsync.ml \
-	mkdtemp.mli \
-	mkdtemp.ml \
-	mkdtemp-c.c \
-	hostname.mli \
+	firstboot.mli \
 	hostname.ml \
-	password.mli \
+	hostname.mli \
 	password.ml \
-	perl_edit.mli \
+	password.mli \
 	perl_edit.ml \
-	planner.mli \
-	planner.ml \
-	progress-c.c \
-	progress.mli \
-	progress.ml \
-	random_seed.mli \
+	perl_edit.mli \
 	random_seed.ml \
-	timezone.mli \
+	random_seed.mli \
 	timezone.ml \
-	tty-c.c \
-	tTY.mli \
-	tTY.ml \
-	urandom.mli \
+	timezone.mli \
 	urandom.ml \
-	uri-c.c \
-	uRI.mli \
-	uRI.ml
+	urandom.mli
 
 if HAVE_OCAML
 
-# Notes:
-# - We're not actually building a functioning program here, we're just
-#   linking everything together to check all the modules build OK.
-# - This list must be in dependency order.
-ocaml_modules = config \
-	libdir \
-	common_gettext \
-	common_utils \
+deps = \
+	$(top_builddir)/mllib/common_gettext.cmx \
+	$(top_builddir)/mllib/common_utils.cmx \
+	crypt-c.o
+
+if HAVE_OCAMLOPT
+OBJECTS = $(deps)
+else
+OBJECTS = $(patsubst %.cmx,%.cmo,$(deps))
+endif
+
+# This list must be in dependency order.
+ocaml_modules = \
+	crypt \
+	firstboot \
+	hostname \
 	urandom \
+	password \
+	perl_edit \
 	random_seed \
-	hostname \
 	timezone \
-	firstboot \
-	perl_edit \
-	tTY \
-	fsync \
-	progress \
-	uRI \
-	crypt \
-	password \
-	mkdtemp \
-	planner
-
-OBJECTS = \
-	$(top_builddir)/fish/guestfish-progress.o \
-	$(top_builddir)/fish/guestfish-uri.o \
-	tty-c.o \
-	fsync-c.o \
-	progress-c.o \
-	uri-c.o \
-	crypt-c.o \
-	mkdtemp-c.o
+	customize_cmdline \
+	customize_run
 
 if HAVE_OCAMLOPT
 OBJECTS += $(patsubst %,%.cmx,$(ocaml_modules))
@@ -104,12 +84,18 @@ else
 OBJECTS += $(patsubst %,%.cmo,$(ocaml_modules))
 endif
 
-noinst_SCRIPTS = dummy
+# XXX virt-customize isn't a complete tool yet, so currently this is
+# just a dummy target binary.
+noinst_SCRIPTS = virt-customize
 
 # -I $(top_builddir)/src/.libs is a hack which forces corresponding -L
 # option to be passed to gcc, so we don't try linking against an
 # installed copy of libguestfs.
-OCAMLPACKAGES = -package str,unix -I $(top_builddir)/src/.libs -I ../ocaml
+OCAMLPACKAGES = \
+	-package str,unix \
+	-I $(top_builddir)/src/.libs \
+	-I $(top_builddir)/ocaml \
+	-I $(top_builddir)/mllib
 if HAVE_OCAML_PKG_GETTEXT
 OCAMLPACKAGES += -package gettext-stub
 endif
@@ -122,7 +108,7 @@ OCAMLCLIBS  = \
 	-L../src/.libs -lutils \
 	-L../gnulib/lib/.libs -lgnu
 
-dummy: $(OBJECTS)
+virt-customize: $(OBJECTS)
 if HAVE_OCAMLOPT
 	$(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) \
 	  mlguestfs.cmxa -linkpkg $^ \
@@ -145,13 +131,6 @@ endif
 .ml.cmx:
 	$(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) -c $< -o $@
 
-# This OCaml module has to be generated by make (configure will put
-# unexpanded prefix macro in).
-
-libdir.ml: Makefile
-	echo 'let libdir = "$(libdir)"' > $@-t
-	mv $@-t $@
-
 # automake will decide we don't need C support in this file.  Really
 # we do, so we have to provide it ourselves.
 
@@ -167,21 +146,9 @@ DEFAULT_INCLUDES = \
 
 # Tests.
 
-check_SCRIPTS = common_utils_tests
-
-if HAVE_OCAMLOPT
-common_utils_tests: common_gettext.cmx common_utils.cmx common_utils_tests.cmx
-	$(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) \
-	  mlguestfs.cmxa -linkpkg $^ -cclib -lncurses -o $@
-else
-common_utils_tests: common_gettext.cmo common_utils.cmo common_utils_tests.cmo
-	$(OCAMLFIND) ocamlc $(OCAMLCFLAGS) \
-	  mlguestfs.cma -linkpkg $^ -cclib -lncurses -custom -o $@
-endif
-
 TESTS_ENVIRONMENT = $(top_builddir)/run --test
 
-TESTS = common_utils_tests
+TESTS =
 
 check-valgrind:
 	$(MAKE) VG="$(top_builddir)/run @VG@" check
@@ -191,7 +158,7 @@ depend: .depend
 
 .depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml)
 	rm -f $@ $@-t
-	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) $^ | \
+	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib $^ | \
 	  $(SED) 's/ *$$//' | \
 	  $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \
 	  $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \
diff --git a/mllib/crypt-c.c b/customize/crypt-c.c
similarity index 100%
rename from mllib/crypt-c.c
rename to customize/crypt-c.c
diff --git a/mllib/crypt.ml b/customize/crypt.ml
similarity index 100%
rename from mllib/crypt.ml
rename to customize/crypt.ml
diff --git a/mllib/crypt.mli b/customize/crypt.mli
similarity index 100%
rename from mllib/crypt.mli
rename to customize/crypt.mli
diff --git a/customize/customize_run.ml b/customize/customize_run.ml
new file mode 100644
index 0000000..dd98017
--- /dev/null
+++ b/customize/customize_run.ml
@@ -0,0 +1,328 @@
+(* virt-customize
+ * 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 Unix
+open Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Customize_cmdline
+open Password
+
+let quote = Filename.quote
+
+let run ~prog ~debug ~quiet (g : Guestfs.guestfs) root (ops : ops) =
+  (* Timestamped messages in ordinary, non-debug non-quiet mode. *)
+  let msg fs = make_message_function ~quiet fs in
+
+  (* Based on the guest type, choose a log file location. *)
+  let logfile =
+    match g#inspect_get_type root with
+    | "windows" | "dos" ->
+      if g#is_dir ~followsymlinks:true "/Temp" then "/Temp/builder.log"
+      else "/builder.log"
+    | _ ->
+      if g#is_dir ~followsymlinks:true "/tmp" then "/tmp/builder.log"
+      else "/builder.log" in
+
+  (* Function to cat the log file, for debugging and error messages. *)
+  let debug_logfile () =
+    try
+      (* XXX If stderr is redirected this actually truncates the
+       * redirection file, which is pretty annoying to say the
+       * least.
+       *)
+      g#download logfile "/dev/stderr"
+    with exn ->
+      eprintf (f_"%s: log file %s: %s (ignored)\n")
+        prog logfile (Printexc.to_string exn) in
+
+  (* Useful wrapper for scripts. *)
+  let do_run ~display cmd =
+    (* Add a prologue to the scripts:
+     * - Pass environment variables through from the host.
+     * - Send stdout and stderr to a log file so we capture all output
+     *   in error messages.
+     * Also catch errors and dump the log file completely on error.
+     *)
+    let env_vars =
+      filter_map (
+        fun name ->
+          try Some (sprintf "export %s=%s" name (quote (Sys.getenv name)))
+          with Not_found -> None
+      ) [ "http_proxy"; "https_proxy"; "ftp_proxy"; "no_proxy" ] in
+    let env_vars = String.concat "\n" env_vars ^ "\n" in
+
+    let cmd = sprintf "\
+exec >>%s 2>&1
+%s
+%s
+" (quote logfile) env_vars cmd in
+
+    if debug then eprintf "running command:\n%s\n%!" cmd;
+    try ignore (g#sh cmd)
+    with
+      Guestfs.Error msg ->
+        debug_logfile ();
+        eprintf (f_"%s: %s: command exited with an error\n") prog display;
+        exit 1
+  in
+
+  (* http://distrowatch.com/dwres.php?resource=package-management *)
+  let guest_install_command packages =
+    let quoted_args = String.concat " " (List.map quote packages) in
+    match g#inspect_get_package_management root with
+    | "apt" ->
+      (* http://unix.stackexchange.com/questions/22820 *)
+      sprintf "
+        export DEBIAN_FRONTEND=noninteractive
+        apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
+        apt-get $apt_opts update
+        apt-get $apt_opts install %s
+      " quoted_args
+    | "pisi" ->
+      sprintf "pisi it %s" quoted_args
+    | "pacman" ->
+      sprintf "pacman -S %s" quoted_args
+    | "urpmi" ->
+      sprintf "urpmi %s" quoted_args
+    | "yum" ->
+      sprintf "yum -y install %s" quoted_args
+    | "zypper" ->
+      (* XXX Should we use -n option? *)
+      sprintf "zypper in %s" quoted_args
+    | "unknown" ->
+      eprintf (f_"%s: --install is not supported for this guest operating system\n")
+        prog;
+      exit 1
+    | pm ->
+      eprintf (f_"%s: sorry, don't know how to use --install with the '%s' package manager\n")
+        prog pm;
+      exit 1
+
+  and guest_update_command () =
+    match g#inspect_get_package_management root with
+    | "apt" ->
+      (* http://unix.stackexchange.com/questions/22820 *)
+      sprintf "
+        export DEBIAN_FRONTEND=noninteractive
+        apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
+        apt-get $apt_opts update
+        apt-get $apt_opts upgrade
+      "
+    | "pisi" ->
+      sprintf "pisi upgrade"
+    | "pacman" ->
+      sprintf "pacman -Su"
+    | "urpmi" ->
+      sprintf "urpmi --auto-select"
+    | "yum" ->
+      sprintf "yum -y update"
+    | "zypper" ->
+      sprintf "zypper update"
+    | "unknown" ->
+      eprintf (f_"%s: --update is not supported for this guest operating system\n")
+        prog;
+      exit 1
+    | pm ->
+      eprintf (f_"%s: sorry, don't know how to use --update with the '%s' package manager\n")
+        prog pm;
+      exit 1
+  in
+
+  (* Set the random seed. *)
+  msg (f_"Setting a random seed");
+  if not (Random_seed.set_random_seed g root) then
+    eprintf (f_"%s: warning: random seed could not be set for this type of guest\n%!") prog;
+
+  (* Used for numbering firstboot commands. *)
+  let i = ref 0 in
+
+  (* Store the passwords and set them all at the end. *)
+  let passwords = Hashtbl.create 13 in
+  let set_password user pw =
+    if Hashtbl.mem passwords user then (
+      eprintf (f_"%s: error: multiple --root-password/--password options set the password for user '%s' twice.\n")
+        prog user;
+      exit 1
+    );
+    Hashtbl.replace passwords user pw
+  in
+
+  (* Perform the remaining customizations in command-line order. *)
+  List.iter (
+    function
+    | `Command cmd ->
+      msg (f_"Running: %s") cmd;
+      do_run ~display:cmd cmd
+
+    | `Delete path ->
+      msg (f_"Deleting: %s") path;
+      g#rm_rf path
+
+    | `Edit (path, expr) ->
+      msg (f_"Editing: %s") path;
+
+      if not (g#is_file path) then (
+        eprintf (f_"%s: error: %s is not a regular file in the guest\n")
+          prog path;
+        exit 1
+      );
+
+      Perl_edit.edit_file ~debug g path expr
+
+    | `FirstbootCommand cmd ->
+      incr i;
+      msg (f_"Installing firstboot command: [%d] %s") !i cmd;
+      Firstboot.add_firstboot_script g root !i cmd
+
+    | `FirstbootPackages pkgs ->
+      incr i;
+      msg (f_"Installing firstboot packages: [%d] %s") !i
+        (String.concat " " pkgs);
+      let cmd = guest_install_command pkgs in
+      Firstboot.add_firstboot_script g root !i cmd
+
+    | `FirstbootScript script ->
+      incr i;
+      msg (f_"Installing firstboot script: [%d] %s") !i script;
+      let cmd = read_whole_file script in
+      Firstboot.add_firstboot_script g root !i cmd
+
+    | `Hostname hostname ->
+      msg (f_"Setting the hostname: %s") hostname;
+      if not (Hostname.set_hostname g root hostname) then
+        eprintf (f_"%s: warning: hostname could not be set for this type of guest\n%!")
+          prog
+
+    | `InstallPackages pkgs ->
+      msg (f_"Installing packages: %s") (String.concat " " pkgs);
+      let cmd = guest_install_command pkgs in
+      do_run ~display:cmd cmd
+
+    | `Link (target, links) ->
+      List.iter (
+        fun link ->
+          msg (f_"Linking: %s -> %s") link target;
+          g#ln_sf target link
+      ) links
+
+    | `Mkdir dir ->
+      msg (f_"Making directory: %s") dir;
+      g#mkdir_p dir
+
+    | `RootPassword pw ->
+      set_password "root" pw
+
+    | `Script script ->
+      msg (f_"Running: %s") script;
+      let cmd = read_whole_file script in
+      do_run ~display:script cmd
+
+    | `Scrub path ->
+      msg (f_"Scrubbing: %s") path;
+      g#scrub_file path
+
+    | `Timezone tz ->
+      msg (f_"Setting the timezone: %s") tz;
+      if not (Timezone.set_timezone ~prog g root tz) then
+        eprintf (f_"%s: warning: timezone could not be set for this type of guest\n%!")
+          prog
+
+    | `Update ->
+      msg (f_"Updating core packages");
+      let cmd = guest_update_command () in
+      do_run ~display:cmd cmd
+
+    | `Upload (path, dest) ->
+      msg (f_"Uploading: %s to %s") path dest;
+      let dest =
+        if g#is_dir ~followsymlinks:true dest then
+          dest ^ "/" ^ Filename.basename path
+        else
+          dest in
+      (* Do the file upload. *)
+      g#upload path dest;
+
+      (* Copy (some of) the permissions from the local file to the
+       * uploaded file.
+       *)
+      let statbuf = stat path in
+      let perms = statbuf.st_perm land 0o7777 (* sticky & set*id *) in
+      g#chmod perms dest;
+      let uid, gid = statbuf.st_uid, statbuf.st_gid in
+      g#chown uid gid dest
+
+    | `Write (path, content) ->
+      msg (f_"Writing: %s") path;
+      g#write path content
+  ) ops.ops;
+
+  (* Set all the passwords at the end. *)
+  if Hashtbl.length passwords > 0 then (
+    match g#inspect_get_type root with
+    | "linux" ->
+      msg (f_"Setting passwords");
+      let password_crypto = ops.flags.password_crypto in
+      set_linux_passwords ~prog ?password_crypto g root passwords
+
+    | _ ->
+      eprintf (f_"%s: warning: passwords could not be set for this type of guest\n%!")
+        prog
+  );
+
+  if ops.flags.selinux_relabel then (
+    msg (f_"SELinux relabelling");
+    let cmd = sprintf "
+      if load_policy && fixfiles restore; then
+        rm -f /.autorelabel
+      else
+        touch /.autorelabel
+        echo '%s: SELinux relabelling failed, will relabel at boot instead.'
+      fi
+    " prog in
+    do_run ~display:"load_policy && fixfiles restore" cmd
+  );
+
+  (* Clean up the log file:
+   *
+   * If debugging, dump out the log file.
+   * Then if asked, scrub the log file.
+   *)
+  if debug then debug_logfile ();
+  if ops.flags.scrub_logfile && g#exists logfile then (
+    msg (f_"Scrubbing the log file");
+
+    (* Try various methods with decreasing complexity. *)
+    try g#scrub_file logfile
+    with _ -> g#rm_f logfile
+  );
+
+  (* Kill any daemons (eg. started by newly installed packages) using
+   * the sysroot.
+   * XXX How to make this nicer?
+   * XXX fuser returns an error if it doesn't kill any processes, which
+   * is not very useful.
+   *)
+  (try ignore (g#debug "sh" [| "fuser"; "-k"; "/sysroot" |])
+   with exn ->
+     if debug then
+       eprintf (f_"%s: %s (ignored)\n") prog (Printexc.to_string exn)
+  );
+  g#ping_daemon () (* tiny delay after kill *)
diff --git a/mllib/timezone.mli b/customize/customize_run.mli
similarity index 68%
copy from mllib/timezone.mli
copy to customize/customize_run.mli
index ad0d4b2..0fa7683 100644
--- a/mllib/timezone.mli
+++ b/customize/customize_run.mli
@@ -1,4 +1,4 @@
-(* Set timezone in virt-sysprep and virt-builder.
+(* virt-customize
  * Copyright (C) 2014 Red Hat Inc.
  *
  * This program is free software; you can redistribute it and/or modify
@@ -16,7 +16,11 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  *)
 
-val set_timezone : prog:string -> Guestfs.guestfs -> string -> string -> bool
-(** [set_timezone ~prog g root "Europe/London"] sets the default timezone
-    of the guest.  Returns [true] if it was able to set the
-    timezone or [false] if not. *)
+(* After command line arguments have been parsed, call this function
+ * to perform the operations on a guest handle.
+ * 
+ * Note that inspection must have been done on the handle, and
+ * filesystems must be mounted up.
+ *)
+
+val run : prog:string -> debug:bool -> quiet:bool -> Guestfs.guestfs -> string -> Customize_cmdline.ops -> unit
diff --git a/mllib/firstboot.ml b/customize/firstboot.ml
similarity index 100%
rename from mllib/firstboot.ml
rename to customize/firstboot.ml
diff --git a/mllib/firstboot.mli b/customize/firstboot.mli
similarity index 100%
rename from mllib/firstboot.mli
rename to customize/firstboot.mli
diff --git a/mllib/hostname.ml b/customize/hostname.ml
similarity index 100%
rename from mllib/hostname.ml
rename to customize/hostname.ml
diff --git a/mllib/hostname.mli b/customize/hostname.mli
similarity index 100%
rename from mllib/hostname.mli
rename to customize/hostname.mli
diff --git a/mllib/password.ml b/customize/password.ml
similarity index 100%
rename from mllib/password.ml
rename to customize/password.ml
diff --git a/mllib/password.mli b/customize/password.mli
similarity index 100%
rename from mllib/password.mli
rename to customize/password.mli
diff --git a/mllib/perl_edit.ml b/customize/perl_edit.ml
similarity index 100%
rename from mllib/perl_edit.ml
rename to customize/perl_edit.ml
diff --git a/mllib/perl_edit.mli b/customize/perl_edit.mli
similarity index 100%
rename from mllib/perl_edit.mli
rename to customize/perl_edit.mli
diff --git a/mllib/random_seed.ml b/customize/random_seed.ml
similarity index 100%
rename from mllib/random_seed.ml
rename to customize/random_seed.ml
diff --git a/mllib/random_seed.mli b/customize/random_seed.mli
similarity index 100%
rename from mllib/random_seed.mli
rename to customize/random_seed.mli
diff --git a/mllib/timezone.ml b/customize/timezone.ml
similarity index 100%
rename from mllib/timezone.ml
rename to customize/timezone.ml
diff --git a/mllib/timezone.mli b/customize/timezone.mli
similarity index 100%
rename from mllib/timezone.mli
rename to customize/timezone.mli
diff --git a/mllib/urandom.ml b/customize/urandom.ml
similarity index 100%
rename from mllib/urandom.ml
rename to customize/urandom.ml
diff --git a/mllib/urandom.mli b/customize/urandom.mli
similarity index 100%
rename from mllib/urandom.mli
rename to customize/urandom.mli
diff --git a/generator/Makefile.am b/generator/Makefile.am
index c129747..e66644c 100644
--- a/generator/Makefile.am
+++ b/generator/Makefile.am
@@ -27,6 +27,7 @@ sources = \
 	c.ml \
 	checks.ml \
 	csharp.ml \
+	customize.ml \
 	daemon.ml \
 	docstrings.ml \
 	erlang.ml \
@@ -89,6 +90,7 @@ objects = \
 	golang.cmo \
 	bindtests.cmo \
 	errnostring.cmo \
+	customize.cmo \
 	main.cmo
 
 EXTRA_DIST = $(sources) files-generated.txt
diff --git a/generator/customize.ml b/generator/customize.ml
new file mode 100644
index 0000000..c87eeba
--- /dev/null
+++ b/generator/customize.ml
@@ -0,0 +1,634 @@
+(* libguestfs
+ * 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
+ *)
+
+(* Please read generator/README first. *)
+
+open Printf
+
+open Docstrings
+open Pr
+
+(* Command-line arguments used by virt-customize, virt-builder and
+ * virt-sysprep.
+ *)
+
+type op = {
+  op_name : string;          (* argument name, without "--" *)
+  op_type : op_type;         (* argument value type *)
+  op_discrim : string;       (* argument discriminator in OCaml code *)
+  op_shortdesc : string;     (* single-line description *)
+  op_pod_longdesc : string;  (* multi-line description *)
+}
+and op_type =
+| Unit                                  (* no argument *)
+| String of string                      (* string *)
+| StringPair of string                  (* string:string *)
+| StringList of string                  (* string,string,... *)
+| TargetLinks of string                 (* target:link[:link...] *)
+| PasswordSelector of string            (* password selector *)
+
+let ops = [
+  { op_name = "delete";
+    op_type = String "PATH";
+    op_discrim = "`Delete";
+    op_shortdesc = "Delete a file or directory";
+    op_pod_longdesc = "\
+Delete a file from the guest.  Or delete a directory (and all its
+contents, recursively).
+
+See also: I<--upload>, I<--scrub>.";
+  };
+  { op_name = "edit";
+    op_type = StringPair "FILE:EXPR";
+    op_discrim = "`Edit";
+    op_shortdesc = "Edit file using Perl expression";
+    op_pod_longdesc = "\
+Edit C<FILE> using the Perl expression C<EXPR>.
+
+Be careful to properly quote the expression to prevent it from
+being altered by the shell.
+
+Note that this option is only available when Perl 5 is installed.
+
+See L<virt-edit(1)/NON-INTERACTIVE EDITING>.";
+  };
+  { op_name = "firstboot";
+    op_type = String "SCRIPT";
+    op_discrim = "`FirstbootScript";
+    op_shortdesc = "Run script at first guest boot";
+    op_pod_longdesc = "\
+Install C<SCRIPT> inside the guest, so that when the guest first boots
+up, the script runs (as root, late in the boot process).
+
+The script is automatically chmod +x after installation in the guest.
+
+The alternative version I<--firstboot-command> is the same, but it
+conveniently wraps the command up in a single line script for you.
+
+You can have multiple I<--firstboot> options.  They run in the same
+order that they appear on the command line.
+
+See also I<--run>.";
+  };
+  { op_name = "firstboot-command";
+    op_type = String "'CMD+ARGS'";
+    op_discrim = "`FirstbootCommand";
+    op_shortdesc = "Run command at first guest boot";
+    op_pod_longdesc = "\
+Run command (and arguments) inside the guest when the guest first
+boots up (as root, late in the boot process).
+
+You can have multiple I<--firstboot> options.  They run in the same
+order that they appear on the command line.
+
+See also I<--run>.";
+  };
+  { op_name = "firstboot-install";
+    op_type = StringList "PKG,PKG..";
+    op_discrim = "`FirstbootPackages";
+    op_shortdesc = "Add package(s) to install at first boot";
+    op_pod_longdesc = "\
+Install the named packages (a comma-separated list).  These are
+installed when the guest first boots using the guest's package manager
+(eg. apt, yum, etc.) and the guest's network connection.
+
+For an overview on the different ways to install packages, see
+L<virt-builder(1)/INSTALLING PACKAGES>.";
+  };
+  { op_name = "hostname";
+    op_type = String "HOSTNAME";
+    op_discrim = "`Hostname";
+    op_shortdesc = "Set the hostname";
+    op_pod_longdesc = "\
+Set the hostname of the guest to C<HOSTNAME>.  You can use a
+dotted hostname.domainname (FQDN) if you want.";
+  };
+  { op_name = "install";
+    op_type = StringList "PKG,PKG..";
+    op_discrim = "`InstallPackages";
+    op_shortdesc = "Add package(s) to install";
+    op_pod_longdesc = "\
+Install the named packages (a comma-separated list).  These are
+installed during the image build using the guest's package manager
+(eg. apt, yum, etc.) and the host's network connection.
+
+For an overview on the different ways to install packages, see
+L<virt-builder(1)/INSTALLING PACKAGES>.
+
+See also I<--update>.";
+  };
+  { op_name = "link";
+    op_type = TargetLinks "TARGET:LINK[:LINK..]";
+    op_discrim = "`Link";
+    op_shortdesc = "Create symbolic links";
+    op_pod_longdesc = "\
+Create symbolic link(s) in the guest, starting at C<LINK> and
+pointing at C<TARGET>.";
+  };
+  { op_name = "mkdir";
+    op_type = String "DIR";
+    op_discrim = "`Mkdir";
+    op_shortdesc = "Create a directory";
+    op_pod_longdesc = "\
+Create a directory in the guest.
+
+This uses S<C<mkdir -p>> so any intermediate directories are created,
+and it also works if the directory already exists.";
+  };
+  { op_name = "root-password";
+    op_type = PasswordSelector "SELECTOR";
+    op_discrim = "`RootPassword";
+    op_shortdesc = "Set root password";
+    op_pod_longdesc = "\
+Set the root password.
+
+See L<virt-builder(1)/USERS AND PASSWORDS> for the format of
+the C<SELECTOR> field, and also how to set up user accounts.
+
+Note: In virt-builder, if you I<don't> set I<--root-password>
+then the guest is given a I<random> root password.";
+  };
+  { op_name = "run";
+    op_type = String "SCRIPT";
+    op_discrim = "`Script";
+    op_shortdesc = "Run script in disk image";
+    op_pod_longdesc = "\
+Run the shell script (or any program) called C<SCRIPT> on the disk
+image.  The script runs virtualized inside a small appliance, chrooted
+into the guest filesystem.
+
+The script is automatically chmod +x.
+
+If libguestfs supports it then a limited network connection is
+available but it only allows outgoing network connections.  You can
+also attach data disks (eg. ISO files) as another way to provide data
+(eg. software packages) to the script without needing a network
+connection (I<--attach>).  You can also upload data files (I<--upload>).
+
+You can have multiple I<--run> options.  They run
+in the same order that they appear on the command line.
+
+See also: I<--firstboot>, I<--attach>, I<--upload>.";
+  };
+  { op_name = "run-command";
+    op_type = String "'CMD+ARGS'";
+    op_discrim = "`Command";
+    op_shortdesc = "Run command in disk image";
+    op_pod_longdesc = "\
+Run the command and arguments on the disk image.  The command runs
+virtualized inside a small appliance, chrooted into the guest filesystem.
+
+If libguestfs supports it then a limited network connection is
+available but it only allows outgoing network connections.  You can
+also attach data disks (eg. ISO files) as another way to provide data
+(eg. software packages) to the script without needing a network
+connection (I<--attach>).  You can also upload data files (I<--upload>).
+
+You can have multiple I<--run-command> options.  They run
+in the same order that they appear on the command line.
+
+See also: I<--firstboot>, I<--attach>, I<--upload>.";
+  };
+  { op_name = "scrub";
+    op_type = String "FILE";
+    op_discrim = "`Scrub";
+    op_shortdesc = "Scrub a file";
+    op_pod_longdesc = "\
+Scrub a file from the guest.  This is like I<--delete> except that:
+
+=over 4
+
+=item *
+
+It scrubs the data so a guest could not recover it.
+
+=item *
+
+It cannot delete directories, only regular files.
+
+=back";
+  };
+  { op_name = "timezone";
+    op_type = String "TIMEZONE";
+    op_discrim = "`Timezone";
+    op_shortdesc = "Set the default timezone";
+    op_pod_longdesc = "\
+Set the default timezone of the guest to C<TIMEZONE>.  Use a location
+string like C<Europe/London>";
+  };
+  { op_name = "update";
+    op_type = Unit;
+    op_discrim = "`Update";
+    op_shortdesc = "Update core packages";
+    op_pod_longdesc = "\
+Do the equivalent of C<yum update>, C<apt-get upgrade>, or whatever
+command is required to update the packages already installed in the
+template to their latest versions.
+
+See also I<--install>.";
+  };
+  { op_name = "upload";
+    op_type = StringPair "FILE:DEST";
+    op_discrim = "`Upload";
+    op_shortdesc = "Upload local file to destination";
+    op_pod_longdesc = "\
+Upload local file C<FILE> to destination C<DEST> in the disk image.
+File owner and permissions from the original are preserved, so you
+should set them to what you want them to be in the disk image.
+
+C<DEST> could be the final filename.  This can be used to rename
+the file on upload.
+
+If C<DEST> is a directory name (which must already exist in the guest)
+then the file is uploaded into that directory, and it keeps the same
+name as on the local filesystem.
+
+See also: I<--mkdir>, I<--delete>, I<--scrub>.";
+  };
+  { op_name = "write";
+    op_type = StringPair "FILE:CONTENT";
+    op_discrim = "`Write";
+    op_shortdesc = "Write file";
+    op_pod_longdesc = "\
+Write C<CONTENT> to C<FILE>.";
+  };
+]
+
+(* Flags. *)
+type flag = {
+  flag_name : string;                (* argument name, without "--" *)
+  flag_type : flag_type;             (* argument value type *)
+  flag_ml_var : string;              (* variable name in OCaml code *)
+  flag_shortdesc : string;           (* single-line description *)
+  flag_pod_longdesc : string;        (* multi-line description *)
+}
+and flag_type =
+| FlagBool of bool                  (* boolean is the default value *)
+| FlagPasswordCrypto of string
+
+let flags = [
+  { flag_name = "no-logfile";
+    flag_type = FlagBool false;
+    flag_ml_var = "scrub_logfile";
+    flag_shortdesc = "Scrub build log file";
+    flag_pod_longdesc = "\
+Scrub C<builder.log> (log file from build commands) from the image
+after building is complete.  If you don't want to reveal precisely how
+the image was built, use this option.
+
+See also: L</LOG FILE>.";
+  };
+  { flag_name = "password-crypto";
+    flag_type = FlagPasswordCrypto "md5|sha256|sha512";
+    flag_ml_var = "password_crypto";
+    flag_shortdesc = "Set password crypto";
+    flag_pod_longdesc = "\
+Set the password encryption to C<md5>, C<sha256> or C<sha512>.
+
+C<sha256> and C<sha512> require glibc E<ge> 2.7 (check crypt(3) inside
+the guest).
+
+C<md5> will work with relatively old Linux guests (eg. RHEL 3), but
+is not secure against modern attacks.
+
+The default is C<sha512> unless libguestfs detects an old guest that
+didn't have support for SHA-512, in which case it will use C<md5>.
+You can override libguestfs by specifying this option.";
+  };
+  { flag_name = "selinux-relabel";
+    flag_type = FlagBool false (* XXX - the default in virt-builder *);
+    flag_ml_var = "selinux_relabel";
+    flag_shortdesc = "Relabel files with correct SELinux labels";
+    flag_pod_longdesc = "\
+Relabel files in the guest so that they have the correct SELinux label.
+
+You should only use this option for guests which support SELinux.";
+  };
+]
+
+let rec generate_customize_cmdline_mli () =
+  generate_header OCamlStyle GPLv2plus;
+
+  pr "\
+(** Command line argument parsing, both for the virt-customize binary
+    and for the other tools that share the same code. *)
+
+";
+  generate_ops_struct_decl ();
+  pr "\n";
+
+  pr "\
+type argspec = Arg.key * Arg.spec * Arg.doc
+val argspec : prog:string -> unit -> (argspec * string option * string) list * (unit -> ops)
+(** This returns a pair [(list, get_ops)].
+
+    [list] is a list of the command line arguments, plus some extra data.
+
+    [get_ops] is a function you can call {i after} command line parsing
+    which will return the actual operations specified by the user on the
+    command line. *)"
+
+and generate_customize_cmdline_ml () =
+  generate_header OCamlStyle GPLv2plus;
+
+  pr "\
+(* Command line argument parsing, both for the virt-customize binary
+ * and for the other tools that share the same code.
+ *)
+
+open Printf
+
+open Common_utils
+open Common_gettext.Gettext
+
+";
+  generate_ops_struct_decl ();
+  pr "\n";
+
+  pr "\
+type argspec = Arg.key * Arg.spec * Arg.doc
+
+let rec argspec ~prog () =
+  let ops = ref [] in
+";
+  List.iter (
+    function
+    | { flag_type = FlagBool default; flag_ml_var = var } ->
+      pr "  let %s = ref %b in\n" var default
+    | { flag_type = FlagPasswordCrypto _; flag_ml_var = var } ->
+      pr "  let %s = ref None in\n" var
+  ) flags;
+  pr "\
+
+  let rec get_ops () = {
+    ops = List.rev !ops;
+    flags = get_flags ();
+  }
+  and get_flags () = {
+";
+  List.iter (fun { flag_ml_var = var } -> pr "    %s = !%s;\n" var var) flags;
+  pr "  }
+  in
+
+  let split_string_pair option_name arg =
+    let i =
+      try String.index arg ':'
+      with Not_found ->
+        eprintf (f_\"%%s: invalid format for '--%%s' parameter, see the man page.\\n\")
+          prog option_name;
+        exit 1 in
+    let len = String.length arg in
+    String.sub arg 0 i, String.sub arg (i+1) (len-(i+1))
+  in
+  let split_string_list arg =
+    string_nsplit \",\" arg
+  in
+  let split_links_list option_name arg =
+    match string_nsplit \":\" arg with
+    | [] | [_] ->
+      eprintf (f_\"%%s: invalid format for '--%%s' parameter, see the man page.\\n\")
+        prog option_name;
+      exit 1
+    | target :: lns -> target, lns
+  in
+
+  let argspec = [
+";
+
+  List.iter (
+    function
+    | { op_type = Unit; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.Unit (fun () -> ops := %s :: !ops),\n" discrim;
+      pr "      \" \" ^ s_\"%s\"\n" shortdesc;
+      pr "    ),\n";
+      pr "    None, %S;\n" longdesc
+    | { op_type = String v; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (fun s -> ops := %s s :: !ops),\n" discrim;
+      pr "      s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+    | { op_type = StringPair v; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (\n";
+      pr "        fun s ->\n";
+      pr "          let p = split_string_pair \"%s\" s in\n" name;
+      pr "          ops := %s p :: !ops\n" discrim;
+      pr "      ),\n";
+      pr "      s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+    | { op_type = StringList v; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (\n";
+      pr "        fun s ->\n";
+      pr "          let ss = split_string_list s in\n";
+      pr "          ops := %s ss :: !ops\n" discrim;
+      pr "      ),\n";
+      pr "      s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+    | { op_type = TargetLinks v; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (\n";
+      pr "        fun s ->\n";
+      pr "          let ss = split_links_list \"%s\" s in\n" name;
+      pr "          ops := %s ss :: !ops\n" discrim;
+      pr "      ),\n";
+      pr "      s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+    | { op_type = PasswordSelector v; op_name = name; op_discrim = discrim;
+        op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (\n";
+      pr "        fun s ->\n";
+      pr "          let sel = Password.parse_selector ~prog s in\n";
+      pr "          ops := %s sel :: !ops\n" discrim;
+      pr "      ),\n";
+      pr "      s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+  ) ops;
+
+  List.iter (
+    function
+    | { flag_type = FlagBool default; flag_ml_var = var; flag_name = name;
+        flag_shortdesc = shortdesc; flag_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      if default (* is true *) then
+        pr "      Arg.Clear %s,\n" var
+      else
+        pr "      Arg.Set %s,\n" var;
+      pr "      \" \" ^ s_\"%s\"\n" shortdesc;
+      pr "    ),\n";
+      pr "    None, %S;\n" longdesc
+    | { flag_type = FlagPasswordCrypto v; flag_ml_var = var;
+        flag_name = name; flag_shortdesc = shortdesc;
+        flag_pod_longdesc = longdesc } ->
+      pr "    (\n";
+      pr "      \"--%s\",\n" name;
+      pr "      Arg.String (\n";
+      pr "        fun s ->\n";
+      pr "          %s := Some (Password.password_crypto_of_string ~prog s)\n" var;
+      pr "      ),\n";
+      pr "      \"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc;
+      pr "    ),\n";
+      pr "    Some %S, %S;\n" v longdesc
+  ) flags;
+
+  pr "  ] in
+
+  argspec, get_ops
+"
+
+and generate_ops_struct_decl () =
+  pr "\
+type ops = {
+  ops : op list;
+  flags : flags;
+}
+";
+
+  (* Operations. *)
+  pr "and op = [\n";
+  List.iter (
+    function
+    | { op_type = Unit; op_discrim = discrim; op_name = name } ->
+      pr "  | %s\n      (* --%s *)\n" discrim name
+    | { op_type = String v; op_discrim = discrim; op_name = name } ->
+      pr "  | %s of string\n      (* --%s %s *)\n" discrim name v
+    | { op_type = StringPair v; op_discrim = discrim;
+        op_name = name } ->
+      pr "  | %s of string * string\n      (* --%s %s *)\n" discrim name v
+    | { op_type = StringList v; op_discrim = discrim;
+        op_name = name } ->
+      pr "  | %s of string list\n      (* --%s %s *)\n" discrim name v
+    | { op_type = TargetLinks v; op_discrim = discrim;
+        op_name = name } ->
+      pr "  | %s of string * string list\n      (* --%s %s *)\n" discrim name v
+    | { op_type = PasswordSelector v; op_discrim = discrim;
+        op_name = name } ->
+      pr "  | %s of Password.password_selector\n      (* --%s %s *)\n"
+        discrim name v
+  ) ops;
+  pr "]\n";
+
+  (* Flags. *)
+  pr "and flags = {\n";
+  List.iter (
+    function
+    | { flag_type = FlagBool _; flag_ml_var = var; flag_name = name } ->
+      pr "  %s : bool;\n      (* --%s *)\n" var name
+    | { flag_type = FlagPasswordCrypto v; flag_ml_var = var;
+        flag_name = name } ->
+      pr "  %s : Password.password_crypto option;\n      (* --%s %s *)\n"
+        var name v
+  ) flags;
+  pr "}\n"
+
+let generate_customize_synopsis_pod () =
+  (* generate_header PODStyle GPLv2plus; - NOT POSSIBLE *)
+
+  let options =
+    List.map (
+      function
+      | { op_type = Unit; op_name = n } ->
+        n, sprintf "[--%s]" n
+      | { op_type = String v | StringPair v | StringList v | TargetLinks v
+            | PasswordSelector v;
+          op_name = n } ->
+        n, sprintf "[--%s %s]" n v
+    ) ops @
+      List.map (
+        function
+        | { flag_type = FlagBool _; flag_name = n } ->
+          n, sprintf "[--%s]" n
+        | { flag_type = FlagPasswordCrypto v; flag_name = n } ->
+          n, sprintf "[--%s %s]" n v
+      ) flags in
+
+  (* Print the option names in the synopsis, line-wrapped. *)
+  let col = ref 4 in
+  pr "   ";
+
+  List.iter (
+    fun (_, str) ->
+      let len = String.length str + 1 in
+      col := !col + len;
+      if !col >= 72 then (
+        col := 4 + len;
+        pr "\n   "
+      );
+      pr " %s" str
+  ) options;
+  if !col > 4 then
+    pr "\n"
+
+let generate_customize_options_pod () =
+  generate_header PODStyle GPLv2plus;
+
+  pr "=over 4\n\n";
+
+  let pod =
+    List.map (
+      function
+      | { op_type = Unit; op_name = n; op_pod_longdesc = ld } ->
+        n, sprintf "B<--%s>" n, ld
+      | { op_type = String v | StringPair v | StringList v | TargetLinks v
+            | PasswordSelector v;
+          op_name = n; op_pod_longdesc = ld } ->
+        n, sprintf "B<--%s> %s" n v, ld
+    ) ops @
+      List.map (
+        function
+        | { flag_type = FlagBool _; flag_name = n; flag_pod_longdesc = ld } ->
+          n, sprintf "B<--%s>" n, ld
+        | { flag_type = FlagPasswordCrypto v;
+            flag_name = n; flag_pod_longdesc = ld } ->
+          n, sprintf "B<--%s> %s" n v, ld
+      ) flags in
+  let cmp (arg1, _, _) (arg2, _, _) =
+    compare (String.lowercase arg1) (String.lowercase arg2)
+  in
+  let pod = List.sort cmp pod in
+
+  List.iter (
+    fun (_, item, longdesc) ->
+      pr "\
+=item %s
+
+%s
+
+" item longdesc
+  ) pod;
+
+  pr "=back\n\n"
diff --git a/generator/main.ml b/generator/main.ml
index d1fa4d2..63ddb9a 100644
--- a/generator/main.ml
+++ b/generator/main.ml
@@ -46,6 +46,7 @@ open Gobject
 open Golang
 open Bindtests
 open Errnostring
+open Customize
 
 let perror msg = function
   | Unix_error (err, _, _) ->
@@ -208,6 +209,11 @@ Run it from the top source directory using the command
     generate_gobject_session_header;
   output_to "gobject/src/session.c" generate_gobject_session_source;
 
+  output_to "customize/customize_cmdline.mli" generate_customize_cmdline_mli;
+  output_to "customize/customize_cmdline.ml" generate_customize_cmdline_ml;
+  output_to "customize/customize-synopsis.pod" generate_customize_synopsis_pod;
+  output_to "customize/customize-options.pod" generate_customize_options_pod;
+
   (* Generate the list of files generated -- last. *)
   printf "generated %d lines of code\n" (get_lines_generated ());
   let files = List.sort compare (get_files_generated ()) in
diff --git a/mllib/Makefile.am b/mllib/Makefile.am
index e275213..fe215f8 100644
--- a/mllib/Makefile.am
+++ b/mllib/Makefile.am
@@ -28,37 +28,20 @@ SOURCES = \
 	common_utils.ml \
 	common_utils_tests.ml \
 	config.ml \
-	crypt-c.c \
-	crypt.ml \
-	crypt.mli \
-	firstboot.mli \
-	firstboot.ml \
 	fsync-c.c \
 	fsync.mli \
 	fsync.ml \
 	mkdtemp.mli \
 	mkdtemp.ml \
 	mkdtemp-c.c \
-	hostname.mli \
-	hostname.ml \
-	password.mli \
-	password.ml \
-	perl_edit.mli \
-	perl_edit.ml \
 	planner.mli \
 	planner.ml \
 	progress-c.c \
 	progress.mli \
 	progress.ml \
-	random_seed.mli \
-	random_seed.ml \
-	timezone.mli \
-	timezone.ml \
 	tty-c.c \
 	tTY.mli \
 	tTY.ml \
-	urandom.mli \
-	urandom.ml \
 	uri-c.c \
 	uRI.mli \
 	uRI.ml
@@ -73,18 +56,10 @@ ocaml_modules = config \
 	libdir \
 	common_gettext \
 	common_utils \
-	urandom \
-	random_seed \
-	hostname \
-	timezone \
-	firstboot \
-	perl_edit \
 	tTY \
 	fsync \
 	progress \
 	uRI \
-	crypt \
-	password \
 	mkdtemp \
 	planner
 
@@ -95,7 +70,6 @@ OBJECTS = \
 	fsync-c.o \
 	progress-c.o \
 	uri-c.o \
-	crypt-c.o \
 	mkdtemp-c.o
 
 if HAVE_OCAMLOPT
diff --git a/po-docs/ja/Makefile.am b/po-docs/ja/Makefile.am
index e954f04..f17be96 100644
--- a/po-docs/ja/Makefile.am
+++ b/po-docs/ja/Makefile.am
@@ -107,6 +107,15 @@ guestfish.1: guestfish.pod guestfish-actions.pod guestfish-commands.pod guestfis
 	  --insert $(srcdir)/guestfish-prepopts.pod:__PREPOPTS__ \
 	  $<
 
+virt-builder.1: virt-builder.pod customize-synopsis.pod customize-options.pod
+	$(PODWRAPPER) \
+	  --no-strict-checks \
+	  --man $@ \
+	  --license GPLv2+ \
+	  --insert $(srcdir)/customize-synopsis.pod:__CUSTOMIZE_SYNOPSIS__ \
+	  --insert $(srcdir)/customize-options.pod:__CUSTOMIZE_OPTIONS__ \
+	  $<
+
 virt-sysprep.1: virt-sysprep.pod sysprep-extra-options.pod sysprep-operations.pod
 	$(PODWRAPPER) \
 	  --no-strict-checks \
diff --git a/po-docs/uk/Makefile.am b/po-docs/uk/Makefile.am
index e954f04..f17be96 100644
--- a/po-docs/uk/Makefile.am
+++ b/po-docs/uk/Makefile.am
@@ -107,6 +107,15 @@ guestfish.1: guestfish.pod guestfish-actions.pod guestfish-commands.pod guestfis
 	  --insert $(srcdir)/guestfish-prepopts.pod:__PREPOPTS__ \
 	  $<
 
+virt-builder.1: virt-builder.pod customize-synopsis.pod customize-options.pod
+	$(PODWRAPPER) \
+	  --no-strict-checks \
+	  --man $@ \
+	  --license GPLv2+ \
+	  --insert $(srcdir)/customize-synopsis.pod:__CUSTOMIZE_SYNOPSIS__ \
+	  --insert $(srcdir)/customize-options.pod:__CUSTOMIZE_OPTIONS__ \
+	  $<
+
 virt-sysprep.1: virt-sysprep.pod sysprep-extra-options.pod sysprep-operations.pod
 	$(PODWRAPPER) \
 	  --no-strict-checks \
diff --git a/po/POTFILES b/po/POTFILES
index ecdbae4..37dbbaa 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -11,6 +11,7 @@ cat/cat.c
 cat/filesystems.c
 cat/ls.c
 cat/visit.c
+customize/crypt-c.c
 daemon/9p.c
 daemon/acl.c
 daemon/augeas.c
@@ -239,7 +240,6 @@ inspector/inspector.c
 java/com_redhat_et_libguestfs_GuestFS.c
 lua/lua-guestfs.c
 make-fs/make-fs.c
-mllib/crypt-c.c
 mllib/fsync-c.c
 mllib/mkdtemp-c.c
 mllib/progress-c.c
diff --git a/po/POTFILES-ml b/po/POTFILES-ml
index ed96697..3870f3d 100644
--- a/po/POTFILES-ml
+++ b/po/POTFILES-ml
@@ -17,21 +17,13 @@ mllib/common_gettext.ml
 mllib/common_utils.ml
 mllib/common_utils_tests.ml
 mllib/config.ml
-mllib/crypt.ml
-mllib/firstboot.ml
 mllib/fsync.ml
-mllib/hostname.ml
 mllib/libdir.ml
 mllib/mkdtemp.ml
-mllib/password.ml
-mllib/perl_edit.ml
 mllib/planner.ml
 mllib/progress.ml
-mllib/random_seed.ml
 mllib/tTY.ml
-mllib/timezone.ml
 mllib/uRI.ml
-mllib/urandom.ml
 resize/resize.ml
 sparsify/cmdline.ml
 sparsify/copying.ml
diff --git a/src/guestfs.pod b/src/guestfs.pod
index b3c32eb..e6e91f4 100644
--- a/src/guestfs.pod
+++ b/src/guestfs.pod
@@ -4272,6 +4272,10 @@ and documentation.
 
 Outside contributions, experimental parts.
 
+=item C<customize>
+
+virt-customize mini-library.
+
 =item C<daemon>
 
 The daemon that runs inside the libguestfs appliance and carries out
diff --git a/sysprep/Makefile.am b/sysprep/Makefile.am
index d20ad08..1bff338 100644
--- a/sysprep/Makefile.am
+++ b/sysprep/Makefile.am
@@ -88,20 +88,20 @@ if HAVE_OCAML
 deps = \
 	$(top_builddir)/mllib/common_gettext.cmx \
 	$(top_builddir)/mllib/common_utils.cmx \
-	$(top_builddir)/fish/guestfish-uri.o \
 	$(top_builddir)/mllib/uri-c.o \
 	$(top_builddir)/mllib/uRI.cmx \
-	$(top_builddir)/mllib/crypt-c.o \
-	$(top_builddir)/mllib/crypt.cmx \
-	$(top_builddir)/mllib/urandom.cmx \
-	$(top_builddir)/mllib/password.cmx \
-	$(top_builddir)/mllib/random_seed.cmx \
-	$(top_builddir)/mllib/hostname.cmx \
-	$(top_builddir)/mllib/timezone.cmx \
-	$(top_builddir)/mllib/firstboot.cmx \
 	$(top_builddir)/mllib/config.cmx \
 	$(top_builddir)/mllib/mkdtemp-c.o \
 	$(top_builddir)/mllib/mkdtemp.cmx \
+	$(top_builddir)/customize/crypt-c.o \
+	$(top_builddir)/customize/crypt.cmx \
+	$(top_builddir)/customize/urandom.cmx \
+	$(top_builddir)/customize/password.cmx \
+	$(top_builddir)/customize/random_seed.cmx \
+	$(top_builddir)/customize/hostname.cmx \
+	$(top_builddir)/customize/timezone.cmx \
+	$(top_builddir)/customize/firstboot.cmx \
+	$(top_builddir)/fish/guestfish-uri.o \
 	sysprep_operation.cmx \
 	$(patsubst %,sysprep_operation_%.cmx,$(operations)) \
 	main.cmx
@@ -121,7 +121,8 @@ OCAMLPACKAGES = \
 	-package str,unix \
 	-I $(top_builddir)/src/.libs \
 	-I $(top_builddir)/ocaml \
-	-I $(top_builddir)/mllib
+	-I $(top_builddir)/mllib \
+	-I $(top_builddir)/customize
 if HAVE_OCAML_PKG_GETTEXT
 OCAMLPACKAGES += -package gettext-stub
 endif
@@ -225,7 +226,7 @@ depend: .depend
 
 .depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml)
 	rm -f $@ $@-t
-	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib $^ | \
+	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib -I $(abs_top_builddir)/customize $^ | \
 	  $(SED) 's/ *$$//' | \
 	  $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \
 	  $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-libvirt/libguestfs.git



More information about the Pkg-libvirt-commits mailing list