cmake: Extracted Zephyr module processing into python script

Fixes: #14513

This commit move the functionality of extracting zephyr modules into
generated CMake and Kconfig include files from CMake into python.

This allows other tools, especially CI to re-use the zephyr module
functionality.

Signed-off-by: Torsten Rasmussen <torsten.rasmussen@nordicsemi.no>
diff --git a/scripts/zephyr_module.py b/scripts/zephyr_module.py
new file mode 100755
index 0000000..0431227
--- /dev/null
+++ b/scripts/zephyr_module.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2019, Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Tool for parsing a list of projects to determine if they are Zephyr
+projects. If no projects are given then the output from `west list` will be
+used as project list.
+
+Include file is generated for Kconfig using --kconfig-out.
+A <name>:<path> text file is generated for use with CMake using --cmake-out.
+'''
+
+import argparse
+import os
+import sys
+import yaml
+import pykwalify.core
+import subprocess
+import re
+
+
+METADATA_SCHEMA = '''
+## A pykwalify schema for basic validation of the structure of a
+## metadata YAML file.
+##
+# The zephyr/module.yml file is a simple list of key value pairs to be used by
+# the build system.
+type: map
+mapping:
+  build:
+    required: true
+    type: map
+    mapping:
+      cmake:
+        required: false
+        type: str
+      kconfig:
+        required: false
+        type: str
+'''
+
+schema = yaml.safe_load(METADATA_SCHEMA)
+
+
+def validate_setting(setting, module_path, filename=None):
+    if setting is not None:
+        if filename is not None:
+            checkfile = os.path.join(module_path, setting, filename)
+        else:
+            checkfile = os.path.join(module_path, setting)
+        if not os.path.isfile(checkfile):
+            return False
+    return True
+
+
+def process_module(module, cmake_out=None, kconfig_out=None):
+    cmake_setting = None
+    kconfig_setting = None
+
+    module_yml = os.path.join(module, 'zephyr/module.yml')
+    if os.path.isfile(module_yml):
+        with open(module_yml, 'r') as f:
+            meta = yaml.safe_load(f.read())
+
+        try:
+            pykwalify.core.Core(source_data=meta, schema_data=schema)\
+                .validate()
+        except pykwalify.errors.SchemaError as e:
+            print('ERROR: Malformed "build" section in file: {}\n{}'
+                  .format(module_yml, e), file=sys.stderr)
+            sys.exit(1)
+
+        section = meta.get('build', dict())
+        cmake_setting = section.get('cmake', None)
+        if not validate_setting(cmake_setting, module, 'CMakeLists.txt'):
+            print('ERROR: "cmake" key in {} has folder value "{}" which '
+                  'does not contain a CMakeLists.txt file.'
+                  .format(module_yml, cmake_setting), file=sys.stderr)
+            sys.exit(1)
+
+        kconfig_setting = section.get('kconfig', None)
+        if not validate_setting(kconfig_setting, module):
+            print('ERROR: "kconfig" key in {} has value "{}" which does not '
+                  'point to a valid Kconfig file.'
+                  .format(module_yml, kconfig_setting), file=sys.stderr)
+            sys.exit(1)
+
+    cmake_path = os.path.join(module, cmake_setting or 'zephyr')
+    cmake_file = os.path.join(cmake_path, 'CMakeLists.txt')
+    if os.path.isfile(cmake_file) and cmake_out is not None:
+        cmake_out.write('{}:{}\n'.format(os.path.basename(module),
+                                         os.path.abspath(cmake_path)))
+
+    kconfig_file = os.path.join(module, kconfig_setting or 'zephyr/Kconfig')
+    if os.path.isfile(kconfig_file) and kconfig_out is not None:
+        kconfig_out.write('osource "{}"\n\n'
+                          .format(os.path.abspath(kconfig_file)))
+
+
+def main():
+    kconfig_out_file = None
+    cmake_out_file = None
+
+    parser = argparse.ArgumentParser(description='''
+    Process a list of projects and create Kconfig / CMake include files for
+    projects which are also a Zephyr module''')
+
+    parser.add_argument('--kconfig-out',
+                        help='File to write with resulting KConfig import'
+                             'statements.')
+    parser.add_argument('--cmake-out',
+                        help='File to write with resulting <name>:<path>'
+                             'values to use for including in CMake')
+    parser.add_argument('-m', '--modules', nargs='+',
+                        help='List of modules to parse instead of using `west'
+                             'list`')
+    parser.add_argument('-x', '--extra-modules', nargs='+',
+                        help='List of extra modules to parse')
+    args = parser.parse_args()
+
+    if args.modules is None:
+        p = subprocess.Popen(['west', 'list', '--format={posixpath}'],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        out, err = p.communicate()
+        if p.returncode == 0:
+            projects = out.decode(sys.getdefaultencoding()).splitlines()
+        elif re.match(r'Error: .* is not in a west installation\..*',
+                      err.decode(sys.getdefaultencoding())):
+            # Only accept the error from bootstrapper in the event we are
+            # outside a west managed project.
+            projects = []
+        else:
+            # A real error occurred, raise an exception
+            raise subprocess.CalledProcessError(cmd=p.args,
+                                                returncode=p.returncode)
+    else:
+        projects = args.modules
+
+    if args.extra_modules is not None:
+        projects += args.extra_modules
+
+    if args.kconfig_out:
+        kconfig_out_file = open(args.kconfig_out, 'w')
+
+    if args.cmake_out:
+        cmake_out_file = open(args.cmake_out, 'w')
+
+    try:
+        for project in projects:
+            # Avoid including Zephyr base project as module.
+            if project != os.environ.get('ZEPHYR_BASE'):
+                process_module(project, cmake_out_file, kconfig_out_file)
+    finally:
+        if args.kconfig_out:
+            kconfig_out_file.close()
+        if args.cmake_out:
+            cmake_out_file.close()
+
+
+if __name__ == "__main__":
+    main()