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
This commit is contained in:
Andrew Scheller 2024-06-05 01:59:28 +01:00 committed by GitHub
parent abce1d427c
commit 115eae7c66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 357 additions and 21 deletions

View File

@ -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 @@ endif ()
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 "")

View File

@ -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 @@ if (CMAKE_BUILD_TYPE STREQUAL "Default")
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)

View File

@ -12,7 +12,7 @@ else()
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}')")

View File

@ -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}')")

View File

@ -10,11 +10,11 @@ macro(add_header_content_from_var VAR)
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

View File

@ -11,7 +11,7 @@ endif()
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 @@ function(pico_set_program_description TARGET description)
# 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()

View File

@ -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 @@ function(pico_clone_default_boot_stage2 NAME)
pico_define_boot_stage2(${NAME} ${PICO_DEFAULT_BOOT_STAGE2_FILE})
endfunction()
pico_promote_common_scope_vars()
pico_promote_common_scope_vars()

View File

@ -18,7 +18,7 @@ if (TARGET tinyusb_device_unmarked)
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}

View File

@ -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)

View File

@ -12,7 +12,7 @@ function(_pico_init_pioasm)
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} )

168
tools/extract_build_defines.py Executable file
View File

@ -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']})

168
tools/extract_cmake_configs.py Executable file
View File

@ -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']})