scripts: Dynamically add driver subsystems to subsystems list

This change extends the parse_syscalls.py script to scan for a
__subsystem sentinal added to driver api declarations. It thens
generates a list that is passed into gen_kobject_list.py to extend
the subsystems list. This allows subsystems to be declared in the
code instead of a separate python list and provides a mechanism for
defining out-of-tree subsystems.

Signed-off-by: Corey Wharton <coreyw7@fb.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c786770..82407c7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -494,6 +494,7 @@
 
 set(syscall_list_h ${CMAKE_CURRENT_BINARY_DIR}/include/generated/syscall_list.h)
 set(syscalls_json  ${CMAKE_CURRENT_BINARY_DIR}/misc/generated/syscalls.json)
+set(subsys_json    ${CMAKE_CURRENT_BINARY_DIR}/misc/generated/subsystems.json)
 
 # The syscalls subdirs txt file is constructed by python containing a list of folders to use for
 # dependency handling, including empty folders.
@@ -585,12 +586,14 @@
 add_custom_command(
   OUTPUT
   ${syscalls_json}
+  ${subsys_json}
   COMMAND
   ${PYTHON_EXECUTABLE}
   ${ZEPHYR_BASE}/scripts/parse_syscalls.py
    --include          ${ZEPHYR_BASE}/include        # Read files from this dir
   ${parse_syscalls_include_args}                    # Read files from these dirs also
-  --json-file        ${syscalls_json}              # Write this file
+  --json-file        ${syscalls_json}               # Write this file
+  --subsystem-file   ${subsys_json}                 # Write subsystem list to this file
   DEPENDS ${syscalls_subdirs_trigger} ${PARSE_SYSCALLS_HEADER_DEPENDS}
   )
 
@@ -617,6 +620,9 @@
   DEPENDS ${syscalls_json}
   )
 
+# This is passed into all calls to the gen_kobject_list.py script.
+set(gen_kobject_list_include_args --include ${subsys_json})
+
 set(DRV_VALIDATION ${PROJECT_BINARY_DIR}/include/generated/driver-validation.h)
 add_custom_command(
   OUTPUT ${DRV_VALIDATION}
@@ -624,8 +630,11 @@
   ${PYTHON_EXECUTABLE}
   ${ZEPHYR_BASE}/scripts/gen_kobject_list.py
   --validation-output ${DRV_VALIDATION}
+  ${gen_kobject_list_include_args}
   $<$<BOOL:${CMAKE_VERBOSE_MAKEFILE}>:--verbose>
-  DEPENDS ${ZEPHYR_BASE}/scripts/gen_kobject_list.py
+  DEPENDS
+  ${ZEPHYR_BASE}/scripts/gen_kobject_list.py
+  ${subsys_json}
   WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
   )
 add_custom_target(${DRIVER_VALIDATION_H_TARGET} DEPENDS ${DRV_VALIDATION})
@@ -941,8 +950,11 @@
     ${GEN_KOBJ_LIST}
     --kernel $<TARGET_FILE:${ZEPHYR_PREBUILT_EXECUTABLE}>
     --gperf-output ${OBJ_LIST}
+    ${gen_kobject_list_include_args}
     $<$<BOOL:${CMAKE_VERBOSE_MAKEFILE}>:--verbose>
-    DEPENDS ${ZEPHYR_PREBUILT_EXECUTABLE}
+    DEPENDS
+    ${ZEPHYR_PREBUILT_EXECUTABLE}
+    ${subsys_json}
     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
     )
   add_custom_target(obj_list DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${OBJ_LIST})
diff --git a/cmake/kobj.cmake b/cmake/kobj.cmake
index bd16d35..f54e148 100644
--- a/cmake/kobj.cmake
+++ b/cmake/kobj.cmake
@@ -21,8 +21,11 @@
     --kobj-types-output ${KOBJ_TYPES}
     --kobj-otype-output ${KOBJ_OTYPE}
     --kobj-size-output ${KOBJ_SIZE}
+    ${gen_kobject_list_include_args}
     $<$<BOOL:${CMAKE_VERBOSE_MAKEFILE}>:--verbose>
-    DEPENDS $ENV{ZEPHYR_BASE}/scripts/gen_kobject_list.py
+    DEPENDS
+    $ENV{ZEPHYR_BASE}/scripts/gen_kobject_list.py
+    ${subsys_json}
     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
     )
   add_custom_target(${KOBJ_TYPES_H_TARGET} DEPENDS ${KOBJ_TYPES} ${KOBJ_OTYPE})
diff --git a/include/toolchain/common.h b/include/toolchain/common.h
index 6f48245..331ed43 100644
--- a/include/toolchain/common.h
+++ b/include/toolchain/common.h
@@ -132,6 +132,11 @@
 #define __syscall
 #endif /* #ifndef ZTEST_UNITTEST */
 
+/* Used as a sentinel by parse_syscalls.py to identify what API structs
+ * define driver subsystems.
+ */
+#define __subsystem
+
 #ifndef BUILD_ASSERT
 /* compile-time assertion that makes the build fail */
 #define BUILD_ASSERT(EXPR) \
diff --git a/scripts/gen_kobject_list.py b/scripts/gen_kobject_list.py
index 5961026..f0f9811 100755
--- a/scripts/gen_kobject_list.py
+++ b/scripts/gen_kobject_list.py
@@ -56,6 +56,7 @@
 import math
 import os
 import struct
+import json
 from elf_helper import ElfHelper, kobject_to_enum
 
 from collections import OrderedDict
@@ -90,8 +91,6 @@
     ("k_futex", (None, True))
 ])
 
-
-
 subsystems = [
     "adc_driver_api",
     "aio_cmp_driver_api",
@@ -331,6 +330,11 @@
             fp.write("#endif\n")
 
 
+def parse_subsystems_list_file(path):
+    with open(path, "r") as fp:
+        subsys_list = json.load(fp)
+    subsystems.extend(subsys_list)
+
 def parse_args():
     global args
 
@@ -355,6 +359,11 @@
     parser.add_argument(
         "-Z", "--kobj-size-output", required=False,
         help="Output case statements for obj_size_get()")
+    parser.add_argument("-i", "--include-subsystem-list", required=False, action='append',
+        help='''Specifies a file with a JSON encoded list of subsystem names to append to
+        the driver subsystems list. Can be specified multiple times:
+        -i file1 -i file2 ...''')
+
     parser.add_argument("-v", "--verbose", action="store_true",
                         help="Print extra debugging information")
     args = parser.parse_args()
@@ -365,6 +374,10 @@
 def main():
     parse_args()
 
+    if args.include_subsystem_list is not None:
+        for list_file in args.include_subsystem_list:
+            parse_subsystems_list_file(list_file)
+
     if args.gperf_output:
         assert args.kernel, "--kernel ELF required for --gperf-output"
         eh = ElfHelper(args.kernel, args.verbose, kobjects, subsystems)
diff --git a/scripts/parse_syscalls.py b/scripts/parse_syscalls.py
index 2d79b53..25c5622 100644
--- a/scripts/parse_syscalls.py
+++ b/scripts/parse_syscalls.py
@@ -5,7 +5,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 """
-Script to scan Zephyr include directories and emit system call metadata
+Script to scan Zephyr include directories and emit system call and subsystem metadata
 
 System calls require a great deal of boilerplate code in order to implement
 completely. This script is the first step in the build system's process of
@@ -26,7 +26,7 @@
 import os
 import json
 
-api_regex = re.compile(r'''
+syscall_regex = re.compile(r'''
 __syscall\s+                    # __syscall attribute, must be first
 ([^(]+)                         # type and name of system call (split later)
 [(]                             # Function opening parenthesis
@@ -34,9 +34,16 @@
 [)]                             # Closing parenthesis
 ''', re.MULTILINE | re.VERBOSE)
 
+subsys_regex = re.compile(r'''
+__subsystem\s+                  # __subsystem attribute, must be first
+struct\s+                       # struct keyword is next
+([^{]+)                         # name of subsystem
+[{]                             # Open curly bracket
+''', re.MULTILINE | re.VERBOSE)
 
 def analyze_headers(multiple_directories):
-    ret = []
+    syscall_ret = []
+    subsys_ret = []
 
     for base_path in multiple_directories:
         for root, dirs, files in os.walk(base_path, topdown=True):
@@ -44,23 +51,41 @@
             files.sort()
             for fn in files:
 
-                # toolchain/common.h has the definition of __syscall which we
+                # toolchain/common.h has the definitions of __syscall and __subsystem which we
                 # don't want to trip over
                 path = os.path.join(root, fn)
                 if not fn.endswith(".h") or path.endswith(os.path.join(os.sep, 'toolchain', 'common.h')):
                     continue
 
                 with open(path, "r", encoding="utf-8") as fp:
-                    try:
-                        result = [(mo.groups(), fn)
-                                  for mo in api_regex.finditer(fp.read())]
-                    except Exception:
-                        sys.stderr.write("While parsing %s\n" % fn)
-                        raise
+                    contents = fp.read()
 
-                    ret.extend(result)
+                try:
+                    syscall_result = [(mo.groups(), fn)
+                                      for mo in syscall_regex.finditer(contents)]
+                    subsys_result = [mo.groups()[0].strip()
+                                     for mo in subsys_regex.finditer(contents)]
+                except Exception:
+                    sys.stderr.write("While parsing %s\n" % fn)
+                    raise
 
-    return ret
+                syscall_ret.extend(syscall_result)
+                subsys_ret.extend(subsys_result)
+
+    return syscall_ret, subsys_ret
+
+
+def update_file_if_changed(path, new):
+    if os.path.exists(path):
+        with open(path, 'r') as fp:
+            old = fp.read()
+
+        if new != old:
+            with open(path, 'w') as fp:
+                fp.write(new)
+    else:
+        with open(path, 'w') as fp:
+            fp.write(new)
 
 
 def parse_args():
@@ -76,34 +101,33 @@
     parser.add_argument(
         "-j", "--json-file", required=True,
         help="Write system call prototype information as json to file")
+    parser.add_argument(
+        "-s", "--subsystem-file", required=True,
+        help="Write subsystem name information as json to file")
     args = parser.parse_args()
 
 
 def main():
     parse_args()
 
-    syscalls = analyze_headers(args.include)
+    syscalls, subsys = analyze_headers(args.include)
+
+    # Only write json files if they don't exist or have changes since
+    # they will force and incremental rebuild.
 
     syscalls_in_json = json.dumps(
         syscalls,
         indent=4,
         sort_keys=True
     )
+    update_file_if_changed(args.json_file, syscalls_in_json)
 
-    # Check if the file already exists, and if there are no changes,
-    # don't touch it since that will force an incremental rebuild
-    path = args.json_file
-    new = syscalls_in_json
-    if os.path.exists(path):
-        with open(path, 'r') as fp:
-            old = fp.read()
-
-        if new != old:
-            with open(path, 'w') as fp:
-                fp.write(new)
-    else:
-        with open(path, 'w') as fp:
-            fp.write(new)
+    subsys_in_json = json.dumps(
+        subsys,
+        indent=4,
+        sort_keys=True
+    )
+    update_file_if_changed(args.subsystem_file, subsys_in_json)
 
 
 if __name__ == "__main__":