configure: automatically parse command line for meson -D options

Right now meson_options.txt lists about 90 options.  Each option
needs code in configure to parse it and pass the option down to Meson as
a -D command-line argument; in addition the default must be duplicated
between configure and meson_options.txt.  This series tries to remove
the code duplication by generating the case statement for those --enable
and --disable options, as well as the corresponding help text.

About 80% of the options can be handled completely by the new mechanism.
Eight meson options are not of the --enable/--disable kind.  Six more need
to be parsed in configure for various reasons documented in the patch,
but they still have their help automatically generated.

The advantages are:

- less code in configure

- parsing and help is more consistent (for example --enable-blobs was
  not supported)

- options are described entirely in one place, meson_options.txt.
  This make it more attractive to use Meson options instead of
  hand-crafted configure options and config-host.mak

A few options change name: --enable-tcmalloc and --enable-jemalloc
become --enable-malloc={tcmalloc,jemalloc}; --disable-blobs becomes
--disable-install-blobs; --enable-trace-backend becomes
--enable-trace-backends.  However, the old names are allowed
for backwards compatibility.

Message-Id: <20211007130829.632254-19-pbonzini@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
diff --git a/scripts/meson-buildoptions.py b/scripts/meson-buildoptions.py
index d48af99..256523c 100755
--- a/scripts/meson-buildoptions.py
+++ b/scripts/meson-buildoptions.py
@@ -25,10 +25,71 @@
 import shlex
 import sys
 
+SKIP_OPTIONS = {
+    "audio_drv_list",
+    "default_devices",
+    "docdir",
+    "fuzzing_engine",
+    "qemu_firmwarepath",
+    "qemu_suffix",
+    "sphinx_build",
+    "trace_file",
+}
+
+LINE_WIDTH = 76
+
+
+# Convert the default value of an option to the string used in
+# the help message
+def value_to_help(value):
+    if isinstance(value, list):
+        return ",".join(value)
+    if isinstance(value, bool):
+        return "enabled" if value else "disabled"
+    return str(value)
+
+
+def wrap(left, text, indent):
+    spaces = " " * indent
+    if len(left) >= indent:
+        yield left
+        left = spaces
+    else:
+        left = (left + spaces)[0:indent]
+    yield from textwrap.wrap(
+        text, width=LINE_WIDTH, initial_indent=left, subsequent_indent=spaces
+    )
+
+
 def sh_print(line=""):
     print('  printf "%s\\n"', shlex.quote(line))
 
 
+def help_line(left, opt, indent, long):
+    right = f'{opt["description"]}'
+    if long:
+        value = value_to_help(opt["value"])
+        if value != "auto":
+            right += f" [{value}]"
+    if "choices" in opt and long:
+        choices = "/".join(sorted(opt["choices"]))
+        right += f" (choices: {choices})"
+    for x in wrap("  " + left, right, indent):
+        sh_print(x)
+
+
+# Return whether the option (a dictionary) can be used with
+# arguments.  Booleans can never be used with arguments;
+# combos allow an argument only if they accept other values
+# than "auto", "enabled", and "disabled".
+def allow_arg(opt):
+    if opt["type"] == "boolean":
+        return False
+    if opt["type"] != "combo":
+        return True
+    return not (set(opt["choices"]) <= {"auto", "disabled", "enabled"})
+
+
 def load_options(json):
     json = [
         x
@@ -42,17 +103,48 @@
 
 def print_help(options):
     print("meson_options_help() {")
+    for opt in options:
+        key = opt["name"].replace("_", "-")
+        # The first section includes options that have an arguments,
+        # and booleans (i.e., only one of enable/disable makes sense)
+        if opt["type"] == "boolean":
+            left = f"--disable-{key}" if opt["value"] else f"--enable-{key}"
+            help_line(left, opt, 27, False)
+        elif allow_arg(opt):
+            if opt["type"] == "combo" and "enabled" in opt["choices"]:
+                left = f"--enable-{key}[=CHOICE]"
+            else:
+                left = f"--enable-{key}=CHOICE"
+            help_line(left, opt, 27, True)
+
     sh_print()
     sh_print("Optional features, enabled with --enable-FEATURE and")
     sh_print("disabled with --disable-FEATURE, default is enabled if available")
     sh_print("(unless built with --without-default-features):")
     sh_print()
+    for opt in options:
+        key = opt["name"].replace("_", "-")
+        if opt["type"] != "boolean" and not allow_arg(opt):
+            help_line(key, opt, 18, False)
     print("}")
 
 
 def print_parse(options):
     print("_meson_option_parse() {")
     print("  case $1 in")
+    for opt in options:
+        key = opt["name"].replace("_", "-")
+        name = opt["name"]
+        if opt["type"] == "boolean":
+            print(f'    --enable-{key}) printf "%s" -D{name}=true ;;')
+            print(f'    --disable-{key}) printf "%s" -D{name}=false ;;')
+        else:
+            if opt["type"] == "combo" and "enabled" in opt["choices"]:
+                print(f'    --enable-{key}) printf "%s" -D{name}=enabled ;;')
+            if opt["type"] == "combo" and "disabled" in opt["choices"]:
+                print(f'    --disable-{key}) printf "%s" -D{name}=disabled ;;')
+            if allow_arg(opt):
+                print(f'    --enable-{key}=*) quote_sh "-D{name}=$2" ;;')
     print("    *) return 1 ;;")
     print("  esac")
     print("}")