sdbus++-gen-meson: create tool for helping with meson

The previous attempt at a meson-helper tool (sdbus++-gendir) had
some usability issues integrating with meson that was not agreeable
to other developers.  The two main complaints were that it did not
automatically catch changes to YAML files and it re-ran the processing
on every YAML file.

The new direction is to create this helper tool which will generate a
tree of meson.build files that can be checked in directly to a
repository and updated whenever a YAML file is added (or removed).

This tool will both create the tree of meson.build necessary for
generating all C++/header and markdown files.  The meson targets created
by the tool also contain callbacks into this tool to abstract details on
which / how sdbus++ is called to generate the output files.

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I410ad2121274b2ba9e6f14985bc7b6a2c92e65e7
diff --git a/tools/meson.build b/tools/meson.build
index e9d3a63..bf5d574 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -1,2 +1,3 @@
 sdbusplusplus_prog = find_program('sdbus++')
 sdbusgen_prog = find_program('sdbus++-gendir')
+sdbuspp_gen_meson_prog = find_program('sdbus++-gen-meson')
diff --git a/tools/sdbus++-gen-meson b/tools/sdbus++-gen-meson
new file mode 100755
index 0000000..a156332
--- /dev/null
+++ b/tools/sdbus++-gen-meson
@@ -0,0 +1,365 @@
+#!/usr/bin/env bash
+
+set -e
+
+function show_usage {
+    cat \
+<<EOF
+Usage: $(basename "$0") [options] <command-args>*
+
+Generate meson.build files from a directory tree containing YAML files and
+facilitate building the sdbus++ sources.
+
+Options:
+    --help              - Display this message
+    --command <cmd>     - Command mode to execute (default 'meson').
+    --directory <path>  - Root directory of the YAML source (default '.').
+    --output <path>     - Root directory of the output (default '.').
+    --tool <path>       - Path to the processing tool (default 'sdbus++').
+    --version           - Display this tool's version string.
+
+Commands:
+    meson               - Generate a tree of meson.build files corresponding
+                          to the source YAML files.
+    cpp <intf>          - Generate the source files from a YAML interface.
+    markdown <intf>     - Generate the markdown files from a YAML interface.
+    version             - Display this tool's version string.
+
+EOF
+}
+
+## The version is somewhat arbitrary but is used to create a warning message
+## if a repository contains old copies of the generated meson.build files and
+## needs an update.  We should increment the version number whenever the
+## resulting meson.build would change.
+tool_version="sdbus++-gen-meson version 1"
+function show_version {
+    echo "$tool_version"
+}
+
+# Set up defaults.
+sdbuspp="sdbus++"
+outputdir="."
+cmd="meson"
+rootdir="."
+
+# Parse options.
+options="$(getopt -o hc:d:o:t:v --long help,command:,directory:,output:,tool:,version -- "$@")"
+eval set -- "$options"
+
+while true;
+do
+    case "$1" in
+        -h | --help)
+            show_usage
+            exit
+            ;;
+
+        -c | --command)
+            shift
+            cmd="$1"
+            shift
+            ;;
+
+        -d | --directory)
+            shift
+            rootdir="$1"
+            shift
+            ;;
+
+        -o | --output)
+            shift
+            outputdir="$1"
+            shift
+            ;;
+
+        -t | --tool)
+            shift
+            sdbuspp="$1"
+            shift
+            ;;
+
+        -v | --version)
+            show_version
+            exit
+            ;;
+
+        --)
+            shift
+            break
+            ;;
+    esac
+done
+
+## Create an initially empty meson.build file.
+## $1 - path to create meson.build at.
+function meson_empty_file {
+    mkdir -p "$1"
+    echo "# Generated file; do not modify." > "$1/meson.build"
+}
+
+## Create the root-level meson.build
+##
+## Inserts rules to run the available version of this tool to ensure the
+## version has not changed.
+function meson_create_root {
+    meson_empty_file "$outputdir"
+
+    cat >> "$outputdir/meson.build" \
+<<EOF
+sdbuspp_gen_meson_ver = run_command(
+    sdbuspp_gen_meson_prog,
+    '--version',
+).stdout().strip().split('\n')[0]
+
+if sdbuspp_gen_meson_ver != '$tool_version'
+    warning('Generated meson files from wrong version of sdbus++-gen-meson.')
+    warning(
+        'Expected "$tool_version", got:',
+        sdbuspp_gen_meson_ver
+    )
+endif
+
+EOF
+}
+
+## hash-tables to store:
+##      meson_paths - list of subdirectory paths for which an empty meson.build
+##                    has already been created.
+##      interfaces - list of interface paths which a YAML has been found and
+##                   which YAML types (interface, errors, etc.).
+declare -A meson_paths
+declare -A interfaces
+
+## Ensure the meson.build files to a path have been created.
+## $1 - The path requiring to be created.
+function meson_create_path {
+
+    meson_path="$outputdir"
+    prev_meson_path=""
+
+    # Split the path into segments.
+    for part in $(echo "$1" | tr '/' '\n');
+    do
+        prev_meson_path="$meson_path"
+        meson_path="$meson_path/$part"
+
+        # Create the meson.build for this segment if it doesn't already exist.
+        if [ "x" == "x${meson_paths[$meson_path]}" ];
+        then
+            meson_paths["$meson_path"]="1"
+            meson_empty_file "$meson_path"
+
+            # Add the 'subdir' link into the parent's meson.build.
+            # We need to skip adding the links into the 'root' meson.build
+            # because most repositories want to selectively add TLDs based
+            # on config flags.  Let them figure out their own logic for that.
+            if [ "x$outputdir" != "x$prev_meson_path" ];
+            then
+                echo "subdir('$part')" >> "$prev_meson_path/meson.build"
+            fi
+        fi
+    done
+}
+
+## Generate the meson target for the source files (.cpp/.hpp) from a YAML
+## interface.
+##
+## $1 - The interface to generate a target for.
+function meson_cpp_target {
+
+    # Determine the source and output files based on the YAMLs present.
+    sources=""
+    outputs=""
+    for s in ${interfaces[$1]};
+    do
+        sources="${sources}meson.source_root() / '$1.$s', "
+
+        case "$s" in
+            errors.yaml)
+                outputs="${outputs}'error.cpp', 'error.hpp', "
+                ;;
+
+            interface.yaml)
+                outputs="${outputs}'server.cpp', 'server.hpp', "
+                outputs="${outputs}'client.hpp', "
+                ;;
+        esac
+    done
+
+    # Create the target to generate the 'outputs'.
+    cat >> "$outputdir/$1/meson.build" \
+<<EOF
+generated_sources += custom_target(
+    '$1__cpp'.underscorify(),
+    input: [ $sources ],
+    output: [ $outputs ],
+    command: [
+        sdbuspp_gen_meson_prog, '--command', 'cpp',
+        '--output', meson.current_build_dir(),
+        '--tool', sdbusplusplus_prog,
+        '--directory', meson.source_root(),
+        '$1',
+    ],
+)
+
+EOF
+}
+
+## Generate the meson target for the markdown files from a YAML interface.
+## $1 - The interface to generate a target for.
+function meson_md_target {
+
+    # Determine the source files based on the YAMLs present.
+    sources=""
+    for s in ${interfaces[$1]};
+    do
+        sources="${sources}meson.source_root() / '$1.$s', "
+    done
+
+    # Create the target to generate the interface.md file.
+    cat >> "$outputdir/$(dirname "$1")/meson.build" \
+<<EOF
+generated_others += custom_target(
+    '$1__markdown'.underscorify(),
+    input: [ $sources ],
+    output: [ '$(basename "$1").md' ],
+    command: [
+        sdbuspp_gen_meson_prog, '--command', 'markdown',
+        '--output', meson.current_build_dir(),
+        '--tool', sdbusplusplus_prog,
+        '--directory', meson.source_root(),
+        '$1',
+    ],
+    build_by_default: true,
+)
+
+EOF
+}
+
+## Handle command=meson by generating the tree of meson.build files.
+function cmd_meson {
+    TLDs="com net org xyz"
+    yamls=""
+
+    # Find all the YAML files in the TLD subdirectories.
+    for d in $TLDs;
+    do
+        dir="$rootdir/$d"
+        if [ ! -d "$dir" ];
+        then
+            continue
+        fi
+
+        yamls="\
+            $yamls \
+            $(find "$dir" -name '*.interface.yaml' -o -name '*.errors.yaml') \
+            "
+    done
+
+    # Sort YAMLs
+    yamls="$(echo "$yamls" | tr " " "\n" | sort)"
+
+    # Assign the YAML files into the hash-table by interface name.
+    for y in $yamls;
+    do
+        rel="$(realpath "--relative-to=$rootdir" "$y")"
+        dir="$(dirname "$rel")"
+        ext="${rel#*.}"
+        base="$(basename "$rel" ".$ext")"
+
+        interfaces["$dir/$base"]="${interfaces[$dir/$base]} $ext"
+    done
+
+    # Create the meson.build files.
+    meson_create_root
+    sorted_ifaces="$(echo "${!interfaces[@]}" | tr " " "\n" | sort)"
+    for i in ${sorted_ifaces};
+    do
+        meson_create_path "$i"
+        meson_cpp_target "$i"
+        meson_md_target "$i"
+    done
+}
+
+## Handle command=cpp by calling sdbus++ as appropriate.
+## $1 - interface to generate.
+##
+## For an interface foo/bar, the outputdir is expected to be foo/bar.
+function cmd_cpp {
+
+    if [ "x" == "x$1" ];
+    then
+        show_usage
+        exit 1
+    fi
+
+    if [ ! -e "$rootdir/$1.interface.yaml" ] && \
+        [ ! -e "$rootdir/$1.errors.yaml" ];
+    then
+        echo "Missing YAML for $1."
+        exit 1
+    fi
+
+    mkdir -p "$outputdir"
+
+    sdbusppcmd="$sdbuspp -r $rootdir"
+    intf="${1//\//.}"
+
+    if [ -e "$rootdir/$1.interface.yaml" ];
+    then
+        $sdbusppcmd interface server-header "$intf" > "$outputdir/server.hpp"
+        $sdbusppcmd interface server-cpp "$intf" > "$outputdir/server.cpp"
+        $sdbusppcmd interface client-header "$intf" > "$outputdir/client.hpp"
+    fi
+
+    if [ -e "$rootdir/$1.errors.yaml" ];
+    then
+        $sdbusppcmd error exception-header "$intf" > "$outputdir/error.hpp"
+        $sdbusppcmd error exception-cpp "$intf" > "$outputdir/error.cpp"
+    fi
+}
+
+## Handle command=markdown by calling sdbus++ as appropriate.
+## $1 - interface to generate.
+##
+## For an interface foo/bar, the outputdir is expected to be foo.
+function cmd_markdown {
+
+    if [ "x" == "x$1" ];
+    then
+        show_usage
+        exit 1
+    fi
+
+    if [ ! -e "$rootdir/$1.interface.yaml" ] && \
+        [ ! -e "$rootdir/$1.errors.yaml" ];
+    then
+        echo "Missing YAML for $1."
+        exit 1
+    fi
+
+    mkdir -p "$outputdir"
+
+    sdbusppcmd="$sdbuspp -r $rootdir"
+    intf="${1//\//.}"
+    base="$(basename "$1")"
+
+    echo -n > "$outputdir/$base.md"
+    if [ -e "$rootdir/$1.interface.yaml" ];
+    then
+        $sdbusppcmd interface markdown "$intf" >> "$outputdir/$base.md"
+    fi
+
+    if [ -e "$rootdir/$1.errors.yaml" ];
+    then
+        $sdbusppcmd error markdown "$intf" >> "$outputdir/$base.md"
+    fi
+}
+
+## Handle command=version.
+function cmd_version {
+    show_version
+}
+
+"cmd_$cmd" "$*"