Add scripts to extract PICO_CMAKE_CONFIG and PICO_BUILD_DEFINE entries (#1708)

Tidy up a couple of PICO_CMAKE_CONFIG and PICO_BUILD_DEFINE entries
diff --git a/cmake/pico_pre_load_platform.cmake b/cmake/pico_pre_load_platform.cmake
index 479eedf..3bce8e6 100644
--- a/cmake/pico_pre_load_platform.cmake
+++ b/cmake/pico_pre_load_platform.cmake
@@ -1,4 +1,4 @@
-# PICO_CMAKE_CONFIG: PICO_PLATFORM, platform to build for e.g. rp2040/host, default=rp2040 or environment value, group=build
+# PICO_CMAKE_CONFIG: PICO_PLATFORM, platform to build for e.g. rp2040/host, type=string, default=rp2040 or environment value, group=build
 if (DEFINED ENV{PICO_PLATFORM} AND (NOT PICO_PLATFORM))
     set(PICO_PLATFORM $ENV{PICO_PLATFORM})
     message("Using PICO_PLATFORM from environment ('${PICO_PLATFORM}')")
@@ -13,7 +13,7 @@
 
 set(PICO_PLATFORM ${PICO_PLATFORM} CACHE STRING "PICO Build platform (e.g. rp2040, host)")
 
-# PICO_CMAKE_CONFIG: PICO_CMAKE_PRELOAD_PLATFORM_FILE, custom CMake file to use to set up the platform environment, default=none, group=build
+# PICO_CMAKE_CONFIG: PICO_CMAKE_PRELOAD_PLATFORM_FILE, custom CMake file to use to set up the platform environment, type=string, group=build
 set(PICO_CMAKE_PRELOAD_PLATFORM_FILE "" CACHE INTERNAL "")
 set(PICO_CMAKE_PRELOAD_PLATFORM_DIR "${CMAKE_CURRENT_LIST_DIR}/preload/platforms" CACHE INTERNAL "")
 
diff --git a/cmake/pico_pre_load_toolchain.cmake b/cmake/pico_pre_load_toolchain.cmake
index b77f421..a8fbbdb 100644
--- a/cmake/pico_pre_load_toolchain.cmake
+++ b/cmake/pico_pre_load_toolchain.cmake
@@ -1,4 +1,4 @@
-# PICO_CMAKE_CONFIG: PICO_TOOLCHAIN_PATH, Path to search for compiler, default=none (i.e. search system paths), group=build
+# PICO_CMAKE_CONFIG: PICO_TOOLCHAIN_PATH, Path to search for compiler, type=string, default=none (i.e. search system paths), group=build
 set(PICO_TOOLCHAIN_PATH "${PICO_TOOLCHAIN_PATH}" CACHE INTERNAL "")
 
 # Set a default build type if none was specified
@@ -16,7 +16,7 @@
     error("Default build type is NOT supported")
 endif()
 
-# PICO_CMAKE_CONFIG: PICO_COMPILER, Optionally specifies a different compiler (other than pico_arm_gcc.cmake) - this is not yet fully supported, default=none, group=build
+# PICO_CMAKE_CONFIG: PICO_COMPILER, Optionally specifies a different compiler (other than pico_arm_gcc.cmake) - this is not yet fully supported, type=string, group=build
 # If PICO_COMPILER is specified, set toolchain file to ${PICO_COMPILER}.cmake.
 if (DEFINED PICO_COMPILER)
     if (DEFINED CMAKE_TOOLCHAIN_FILE)
diff --git a/src/board_setup.cmake b/src/board_setup.cmake
index 153af4e..48839f7 100644
--- a/src/board_setup.cmake
+++ b/src/board_setup.cmake
@@ -12,7 +12,7 @@
 endif()
 set(PICO_BOARD ${PICO_BOARD} CACHE STRING "PICO target board (e.g. pico)" FORCE)
 
-# PICO_CMAKE_CONFIG: PICO_BOARD_CMAKE_DIRS, Directories to look for <PICO_BOARD>.cmake in. This is overridable from the user environment, type=list, default="", group=build
+# PICO_CMAKE_CONFIG: PICO_BOARD_CMAKE_DIRS, Directories to look for <PICO_BOARD>.cmake in. This is overridable from the user environment, type=list, group=build
 if (DEFINED ENV{PICO_BOARD_CMAKE_DIRS})
     set(PICO_BOARD_CMAKE_DIRS $ENV{PICO_BOARD_CMAKE_DIRS})
     message("Using PICO_BOARD_CMAKE_DIRS from environment ('${PICO_BOARD_CMAKE_DIRS}')")
diff --git a/src/boards/generic_board.cmake b/src/boards/generic_board.cmake
index 3dd5292..3307e78 100644
--- a/src/boards/generic_board.cmake
+++ b/src/boards/generic_board.cmake
@@ -1,6 +1,6 @@
 # For boards without their own cmake file, simply include a header
 
-# PICO_CMAKE_CONFIG: PICO_BOARD_HEADER_DIRS, Directories to look for <PICO_BOARD>.h in. This is overridable from the user environment, type=list, default="", group=build
+# PICO_CMAKE_CONFIG: PICO_BOARD_HEADER_DIRS, Directories to look for <PICO_BOARD>.h in. This is overridable from the user environment, type=list, group=build
 if (DEFINED ENV{PICO_BOARD_HEADER_DIRS})
     set(PICO_BOARD_HEADER_DIRS $ENV{PICO_BOARD_HEADER_DIRS})
     message("Using PICO_BOARD_HEADER_DIRS from environment ('${PICO_BOARD_HEADER_DIRS}')")
diff --git a/src/common/pico_base/generate_config_header.cmake b/src/common/pico_base/generate_config_header.cmake
index 9840105..5cc6aff 100644
--- a/src/common/pico_base/generate_config_header.cmake
+++ b/src/common/pico_base/generate_config_header.cmake
@@ -10,11 +10,11 @@
     endforeach()
 endmacro()
 
-# PICO_CMAKE_CONFIG: PICO_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for all platforms, type=list, default="", group=pico_base
+# PICO_CMAKE_CONFIG: PICO_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for all platforms, type=list, group=pico_base
 add_header_content_from_var(PICO_CONFIG_HEADER_FILES)
 
-# PICO_CMAKE_CONFIG: PICO_RP2040_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for rp2040 platform, type=list, default="", group=pico_base
-# PICO_CMAKE_CONFIG: PICO_HOST_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for host platform, type=list, default="", group=pico_base
+# PICO_CMAKE_CONFIG: PICO_RP2040_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for rp2040 platform, type=list, group=pico_base
+# PICO_CMAKE_CONFIG: PICO_HOST_CONFIG_HEADER_FILES, List of extra header files to include from pico/config.h for host platform, type=list, group=pico_base
 add_header_content_from_var(PICO_${PICO_PLATFORM_UPPER}_CONFIG_HEADER_FILES)
 
 file(GENERATE
diff --git a/src/common/pico_binary_info/CMakeLists.txt b/src/common/pico_binary_info/CMakeLists.txt
index eb0c3f6..bcaad6f 100644
--- a/src/common/pico_binary_info/CMakeLists.txt
+++ b/src/common/pico_binary_info/CMakeLists.txt
@@ -11,7 +11,7 @@
 target_link_libraries(pico_binary_info INTERFACE pico_binary_info_headers)
 
 function(pico_set_program_name TARGET name)
-    # PICO_BUILD_DEFINE: PICO_PROGRAM_NAME, value passed to pico_set_program_name, type=string, default=none, group=pico_binary_info
+    # PICO_BUILD_DEFINE: PICO_PROGRAM_NAME, value passed to pico_set_program_name, type=string, group=pico_binary_info
     target_compile_definitions(${TARGET} PRIVATE -DPICO_PROGRAM_NAME="${name}")
 endfunction()
 
@@ -19,16 +19,16 @@
     # since this is the command line, we will remove newlines
     string(REPLACE "\n" " " description ${description})
     string(REPLACE "\"" "\\\"" description ${description})
-    # PICO_BUILD_DEFINE: PICO_PROGRAM_DESCRIPTION, value passed to pico_set_program_description, type=string, default=none, group=pico_binary_info
+    # PICO_BUILD_DEFINE: PICO_PROGRAM_DESCRIPTION, value passed to pico_set_program_description, type=string, group=pico_binary_info
     target_compile_definitions(${TARGET} PRIVATE -DPICO_PROGRAM_DESCRIPTION="${description}")
 endfunction()
 
 function(pico_set_program_url TARGET url)
-    # PICO_BUILD_DEFINE: PICO_PROGRAM_URL, value passed to pico_set_program_url, type=string, default=none, group=pico_binary_info
+    # PICO_BUILD_DEFINE: PICO_PROGRAM_URL, value passed to pico_set_program_url, type=string, group=pico_binary_info
     target_compile_definitions(${TARGET} PRIVATE -DPICO_PROGRAM_URL="${url}")
 endfunction()
 
 function(pico_set_program_version TARGET version)
-    # PICO_BUILD_DEFINE: PICO_PROGRAM_VERSION_STRING, value passed to pico_set_program_version, type=string, default=none, group=pico_binary_info
+    # PICO_BUILD_DEFINE: PICO_PROGRAM_VERSION_STRING, value passed to pico_set_program_version, type=string, group=pico_binary_info
     target_compile_definitions(${TARGET} PRIVATE -DPICO_PROGRAM_VERSION_STRING="${version}")
 endfunction()
diff --git a/src/rp2_common/boot_stage2/CMakeLists.txt b/src/rp2_common/boot_stage2/CMakeLists.txt
index 97c8e01..2636373 100644
--- a/src/rp2_common/boot_stage2/CMakeLists.txt
+++ b/src/rp2_common/boot_stage2/CMakeLists.txt
@@ -1,5 +1,5 @@
-# PICO_CMAKE_CONFIG: PICO_DEFAULT_BOOT_STAGE2_FILE, Default boot stage 2 file to use unless overridden by pico_set_boot_stage2 on the TARGET; this setting is useful when explicitly setting the default build from a per board CMake file, group=build
-# PICO_CMAKE_CONFIG: PICO_DEFAULT_BOOT_STAGE2, Simpler alternative to specifying PICO_DEFAULT_BOOT_STAGE2_FILE where the file is src/rp2_common/boot_stage2/{PICO_DEFAULT_BOOT_STAGE2}.S, default=compile_time_choice, group=build
+# PICO_CMAKE_CONFIG: PICO_DEFAULT_BOOT_STAGE2_FILE, Default boot stage 2 file to use unless overridden by pico_set_boot_stage2 on the TARGET; this setting is useful when explicitly setting the default build from a per board CMake file, type=string, group=build
+# PICO_CMAKE_CONFIG: PICO_DEFAULT_BOOT_STAGE2, Simpler alternative to specifying PICO_DEFAULT_BOOT_STAGE2_FILE where the file is src/rp2_common/boot_stage2/{PICO_DEFAULT_BOOT_STAGE2}.S, type=string, default=compile_time_choice, group=build
 
 if (DEFINED ENV{PICO_DEFAULT_BOOT_STAGE2_FILE})
     set(PICO_DEFAULT_BOOT_STAGE2_FILE $ENV{PICO_DEFAULT_BOOT_STAGE2_FILE})
@@ -105,4 +105,4 @@
     pico_define_boot_stage2(${NAME} ${PICO_DEFAULT_BOOT_STAGE2_FILE})
 endfunction()
 
-pico_promote_common_scope_vars()
\ No newline at end of file
+pico_promote_common_scope_vars()
diff --git a/src/rp2_common/pico_stdio_usb/CMakeLists.txt b/src/rp2_common/pico_stdio_usb/CMakeLists.txt
index 9113c71..bca4d35 100644
--- a/src/rp2_common/pico_stdio_usb/CMakeLists.txt
+++ b/src/rp2_common/pico_stdio_usb/CMakeLists.txt
@@ -18,7 +18,7 @@
     target_link_libraries(pico_stdio_usb INTERFACE
         tinyusb_device_unmarked
     )
-    # PICO_CMAKE_CONFIG: PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS, Maximum number of milliseconds to wait during initialization for a CDC connection from the host (negative means indefinite) during initialization, default=0, group=pico_stdio_usb
+    # PICO_CMAKE_CONFIG: PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS, Maximum number of milliseconds to wait during initialization for a CDC connection from the host (negative means indefinite) during initialization, type=int, default=0, group=pico_stdio_usb
     if (PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS)
         target_compile_definitions(pico_stdio_usb INTERFACE
             PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=${PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS}
diff --git a/src/rp2_common/pico_stdlib/CMakeLists.txt b/src/rp2_common/pico_stdlib/CMakeLists.txt
index 183ca52..b5ad182 100644
--- a/src/rp2_common/pico_stdlib/CMakeLists.txt
+++ b/src/rp2_common/pico_stdlib/CMakeLists.txt
@@ -1,8 +1,8 @@
-# PICO_CMAKE_CONFIG: PICO_STDIO_UART, OPTION: Globally enable stdio UART, default=1, group=pico_stdlib
+# PICO_CMAKE_CONFIG: PICO_STDIO_UART, OPTION: Globally enable stdio UART, type=bool, default=1, group=pico_stdlib
 option(PICO_STDIO_UART "Globally enable stdio UART" 1)
-# PICO_CMAKE_CONFIG: PICO_STDIO_USB, OPTION: Globally enable stdio USB, default=0, group=pico_stdlib
+# PICO_CMAKE_CONFIG: PICO_STDIO_USB, OPTION: Globally enable stdio USB, type=bool, default=0, group=pico_stdlib
 option(PICO_STDIO_USB "Globally enable stdio USB" 0)
-# PICO_CMAKE_CONFIG: PICO_STDIO_SEMIHOSTING, OPTION: Globally enable stdio semihosting, default=0, group=pico_stdlib
+# PICO_CMAKE_CONFIG: PICO_STDIO_SEMIHOSTING, OPTION: Globally enable stdio semihosting, type=bool, default=0, group=pico_stdlib
 option(PICO_STDIO_SEMIHOSTING "Globally enable stdio semi-hosting" 0)
 
 if (NOT TARGET pico_stdlib)
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index b358142..9082a53 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -12,7 +12,7 @@
     endif()
 endfunction()
 
-# PICO_CMAKE_CONFIG: PICO_DEFAULT_PIOASM_OUTPUT_FORMAT, default output format used by pioasm when using pico_generate_pio_header, default=c-sdk, group=build
+# PICO_CMAKE_CONFIG: PICO_DEFAULT_PIOASM_OUTPUT_FORMAT, default output format used by pioasm when using pico_generate_pio_header, type=string, default=c-sdk, group=build
 function(pico_generate_pio_header TARGET PIO)
     _pico_init_pioasm()
     cmake_parse_arguments(pico_generate_pio_header "" "OUTPUT_FORMAT;OUTPUT_DIR" "" ${ARGN} )
diff --git a/tools/extract_build_defines.py b/tools/extract_build_defines.py
new file mode 100755
index 0000000..221a255
--- /dev/null
+++ b/tools/extract_build_defines.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#
+# Script to scan the Raspberry Pi Pico SDK tree searching for CMake build defines
+# Outputs a tab separated file of the configuration item:
+# name	location	description	type	default	group
+#
+# Usage:
+#
+# tools/extract_build_defines.py <root of repo> [output file]
+#
+# If not specified, output file will be `pico_build_defines.tsv`
+
+
+import os
+import sys
+import re
+import csv
+import logging
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+scandir = sys.argv[1]
+outfile = sys.argv[2] if len(sys.argv) > 2 else 'pico_build_defines.tsv'
+
+BUILD_DEFINE_RE = re.compile(r'#\s+PICO_BUILD_DEFINE:\s+(\w+),\s+([^,]+)(?:,\s+(.*))?$')
+
+all_configs = {}
+all_attrs = set()
+all_descriptions = {}
+
+
+
+def ValidateAttrs(config_attrs, file_path, linenum):
+    _type = config_attrs.get('type')
+
+    # Validate attrs
+    if _type == 'int':
+        _min = _max = _default = None
+        if config_attrs.get('min', None) is not None:
+            value = config_attrs['min']
+            m = re.match(r'^(\d+)e(\d+)$', value.lower())
+            if m:
+                _min = int(m.group(1)) * 10**int(m.group(2))
+            else:
+                _min = int(value, 0)
+        if config_attrs.get('max', None) is not None:
+            value = config_attrs['max']
+            m = re.match(r'^(\d+)e(\d+)$', value.lower())
+            if m:
+                _max = int(m.group(1)) * 10**int(m.group(2))
+            else:
+                _max = int(value, 0)
+        if config_attrs.get('default', None) is not None:
+            if '/' not in config_attrs['default']:
+                try:
+                    value = config_attrs['default']
+                    m = re.match(r'^(\d+)e(\d+)$', value.lower())
+                    if m:
+                        _default = int(m.group(1)) * 10**int(m.group(2))
+                    else:
+                        _default = int(value, 0)
+                except ValueError:
+                    pass
+        if _min is not None and _max is not None:
+            if _min > _max:
+                raise Exception('{} at {}:{} has min {} > max {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['max']))
+        if _min is not None and _default is not None:
+            if _min > _default:
+                raise Exception('{} at {}:{} has min {} > default {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['default']))
+        if _default is not None and _max is not None:
+            if _default > _max:
+                raise Exception('{} at {}:{} has default {} > max {}'.format(config_name, file_path, linenum, config_attrs['default'], config_attrs['max']))
+    elif _type == 'bool':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+        if _default is not None:
+            if '/' not in _default:
+                if (_default.lower() != '0') and (config_attrs['default'].lower() != '1') and ( _default not in all_configs):
+                    logger.info('{} at {}:{} has non-integer default value "{}"'.format(config_name, file_path, linenum, config_attrs['default']))
+
+    elif _type == 'string':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+    elif _type == 'list':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+    else:
+        raise Exception("Found unknown PICO_BUILD_DEFINE type {} at {}:{}".format(_type, file_path, linenum))
+
+
+
+
+# Scan all CMakeLists.txt and .cmake files in the specific path, recursively.
+
+for dirpath, dirnames, filenames in os.walk(scandir):
+    for filename in filenames:
+        file_ext = os.path.splitext(filename)[1]
+        if filename == 'CMakeLists.txt' or file_ext == '.cmake':
+            file_path = os.path.join(dirpath, filename)
+
+            with open(file_path, encoding="ISO-8859-1") as fh:
+                linenum = 0
+                for line in fh.readlines():
+                    linenum += 1
+                    line = line.strip()
+                    m = BUILD_DEFINE_RE.match(line)
+                    if m:
+                        config_name = m.group(1)
+                        config_description = m.group(2)
+                        _attrs = m.group(3)
+                        # allow commas to appear inside brackets by converting them to and from NULL chars
+                        _attrs = re.sub(r'(\(.+\))', lambda m: m.group(1).replace(',', '\0'), _attrs)
+
+                        if '=' in config_description:
+                            raise Exception("For {} at {}:{} the description was set to '{}' - has the description field been omitted?".format(config_name, file_path, linenum, config_description))
+                        if config_description in all_descriptions:
+                            raise Exception("Found description {} at {}:{} but it was already used at {}:{}".format(config_description, file_path, linenum, os.path.join(scandir, all_descriptions[config_description]['filename']), all_descriptions[config_description]['line_number']))
+                        else:
+                            all_descriptions[config_description] = {'config_name': config_name, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum}
+
+                        config_attrs = {}
+                        prev = None
+                        # Handle case where attr value contains a comma
+                        for item in _attrs.split(','):
+                            if "=" not in item:
+                                assert(prev)
+                                item = prev + "," + item
+                            try:
+                                k, v = (i.strip() for i in item.split('='))
+                            except ValueError:
+                                raise Exception('{} at {}:{} has malformed value {}'.format(config_name, file_path, linenum, item))
+                            config_attrs[k] = v.replace('\0', ',')
+                            all_attrs.add(k)
+                            prev = item
+                        #print(file_path, config_name, config_attrs)
+
+                        if 'group' not in config_attrs:
+                            raise Exception('{} at {}:{} has no group attribute'.format(config_name, file_path, linenum))
+
+                        #print(file_path, config_name, config_attrs)
+                        if config_name in all_configs:
+                            raise Exception("Found {} at {}:{} but it was already declared at {}:{}".format(config_name, file_path, linenum, os.path.join(scandir, all_configs[config_name]['filename']), all_configs[config_name]['line_number']))
+                        else:
+                            all_configs[config_name] = {'attrs': config_attrs, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum, 'description': config_description}
+
+
+for config_name, config_obj in all_configs.items():
+    file_path = os.path.join(scandir, config_obj['filename'])
+    linenum = config_obj['line_number']
+
+    ValidateAttrs(config_obj['attrs'], file_path, linenum)
+
+with open(outfile, 'w', newline='') as csvfile:
+    fieldnames = ('name', 'location', 'description', 'type') + tuple(sorted(all_attrs - set(['type'])))
+    writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore', dialect='excel-tab')
+
+    writer.writeheader()
+    for config_name, config_obj in sorted(all_configs.items()):
+        writer.writerow({'name': config_name, 'location': '/{}:{}'.format(config_obj['filename'], config_obj['line_number']), 'description': config_obj['description'], **config_obj['attrs']})
diff --git a/tools/extract_cmake_configs.py b/tools/extract_cmake_configs.py
new file mode 100755
index 0000000..9c4f194
--- /dev/null
+++ b/tools/extract_cmake_configs.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#
+# Script to scan the Raspberry Pi Pico SDK tree searching for CMake configuration items
+# Outputs a tab separated file of the configuration item:
+# name	location	description	type	advanced    default	group
+#
+# Usage:
+#
+# tools/extract_cmake_configs.py <root of repo> [output file]
+#
+# If not specified, output file will be `pico_cmake_configs.tsv`
+
+
+import os
+import sys
+import re
+import csv
+import logging
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+scandir = sys.argv[1]
+outfile = sys.argv[2] if len(sys.argv) > 2 else 'pico_cmake_configs.tsv'
+
+CMAKE_CONFIG_RE = re.compile(r'#\s+PICO_CMAKE_CONFIG:\s+(\w+),\s+([^,]+)(?:,\s+(.*))?$')
+
+all_configs = {}
+all_attrs = set()
+all_descriptions = {}
+
+
+
+def ValidateAttrs(config_attrs, file_path, linenum):
+    _type = config_attrs.get('type')
+
+    # Validate attrs
+    if _type == 'int':
+        _min = _max = _default = None
+        if config_attrs.get('min', None) is not None:
+            value = config_attrs['min']
+            m = re.match(r'^(\d+)e(\d+)$', value.lower())
+            if m:
+                _min = int(m.group(1)) * 10**int(m.group(2))
+            else:
+                _min = int(value, 0)
+        if config_attrs.get('max', None) is not None:
+            value = config_attrs['max']
+            m = re.match(r'^(\d+)e(\d+)$', value.lower())
+            if m:
+                _max = int(m.group(1)) * 10**int(m.group(2))
+            else:
+                _max = int(value, 0)
+        if config_attrs.get('default', None) is not None:
+            if '/' not in config_attrs['default']:
+                try:
+                    value = config_attrs['default']
+                    m = re.match(r'^(\d+)e(\d+)$', value.lower())
+                    if m:
+                        _default = int(m.group(1)) * 10**int(m.group(2))
+                    else:
+                        _default = int(value, 0)
+                except ValueError:
+                    pass
+        if _min is not None and _max is not None:
+            if _min > _max:
+                raise Exception('{} at {}:{} has min {} > max {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['max']))
+        if _min is not None and _default is not None:
+            if _min > _default:
+                raise Exception('{} at {}:{} has min {} > default {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['default']))
+        if _default is not None and _max is not None:
+            if _default > _max:
+                raise Exception('{} at {}:{} has default {} > max {}'.format(config_name, file_path, linenum, config_attrs['default'], config_attrs['max']))
+    elif _type == 'bool':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+        if _default is not None:
+            if '/' not in _default:
+                if (_default.lower() != '0') and (config_attrs['default'].lower() != '1') and ( _default not in all_configs):
+                    logger.info('{} at {}:{} has non-integer default value "{}"'.format(config_name, file_path, linenum, config_attrs['default']))
+
+    elif _type == 'string':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+    elif _type == 'list':
+        assert 'min' not in config_attrs
+        assert 'max' not in config_attrs
+        _default = config_attrs.get('default', None)
+    else:
+        raise Exception("Found unknown PICO_CMAKE_CONFIG type {} at {}:{}".format(_type, file_path, linenum))
+
+
+
+
+# Scan all CMakeLists.txt and .cmake files in the specific path, recursively.
+
+for dirpath, dirnames, filenames in os.walk(scandir):
+    for filename in filenames:
+        file_ext = os.path.splitext(filename)[1]
+        if filename == 'CMakeLists.txt' or file_ext == '.cmake':
+            file_path = os.path.join(dirpath, filename)
+
+            with open(file_path, encoding="ISO-8859-1") as fh:
+                linenum = 0
+                for line in fh.readlines():
+                    linenum += 1
+                    line = line.strip()
+                    m = CMAKE_CONFIG_RE.match(line)
+                    if m:
+                        config_name = m.group(1)
+                        config_description = m.group(2)
+                        _attrs = m.group(3)
+                        # allow commas to appear inside brackets by converting them to and from NULL chars
+                        _attrs = re.sub(r'(\(.+\))', lambda m: m.group(1).replace(',', '\0'), _attrs)
+
+                        if '=' in config_description:
+                            raise Exception("For {} at {}:{} the description was set to '{}' - has the description field been omitted?".format(config_name, file_path, linenum, config_description))
+                        if config_description in all_descriptions:
+                            raise Exception("Found description {} at {}:{} but it was already used at {}:{}".format(config_description, file_path, linenum, os.path.join(scandir, all_descriptions[config_description]['filename']), all_descriptions[config_description]['line_number']))
+                        else:
+                            all_descriptions[config_description] = {'config_name': config_name, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum}
+
+                        config_attrs = {}
+                        prev = None
+                        # Handle case where attr value contains a comma
+                        for item in _attrs.split(','):
+                            if "=" not in item:
+                                assert(prev)
+                                item = prev + "," + item
+                            try:
+                                k, v = (i.strip() for i in item.split('='))
+                            except ValueError:
+                                raise Exception('{} at {}:{} has malformed value {}'.format(config_name, file_path, linenum, item))
+                            config_attrs[k] = v.replace('\0', ',')
+                            all_attrs.add(k)
+                            prev = item
+                        #print(file_path, config_name, config_attrs)
+
+                        if 'group' not in config_attrs:
+                            raise Exception('{} at {}:{} has no group attribute'.format(config_name, file_path, linenum))
+
+                        #print(file_path, config_name, config_attrs)
+                        if config_name in all_configs:
+                            raise Exception("Found {} at {}:{} but it was already declared at {}:{}".format(config_name, file_path, linenum, os.path.join(scandir, all_configs[config_name]['filename']), all_configs[config_name]['line_number']))
+                        else:
+                            all_configs[config_name] = {'attrs': config_attrs, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum, 'description': config_description}
+
+
+for config_name, config_obj in all_configs.items():
+    file_path = os.path.join(scandir, config_obj['filename'])
+    linenum = config_obj['line_number']
+
+    ValidateAttrs(config_obj['attrs'], file_path, linenum)
+
+with open(outfile, 'w', newline='') as csvfile:
+    fieldnames = ('name', 'location', 'description', 'type') + tuple(sorted(all_attrs - set(['type'])))
+    writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore', dialect='excel-tab')
+
+    writer.writeheader()
+    for config_name, config_obj in sorted(all_configs.items()):
+        writer.writerow({'name': config_name, 'location': '/{}:{}'.format(config_obj['filename'], config_obj['line_number']), 'description': config_obj['description'], **config_obj['attrs']})