# SPDX-License-Identifier: Apache-2.0 # # Copyright (c) 2024, Nordic Semiconductor ASA # CMake YAML module for handling of YAML files. # # This module offers basic support for simple yaml files. # # It supports basic key-value pairs, like # foo: bar # # basic key-object pairs, like # foo: # bar: baz # # Simple value lists, like: # foos: # - foo1 # - foo2 # - foo3 # # Support for list of maps, like: # foo: # - bar: val1 # baz: val1 # - bar: val2 # baz: val2 # # All of above can be combined, for example like: # foo: # bar: baz # quz: # greek: # - alpha # - beta # - gamma # fred: thud include_guard(GLOBAL) include(extensions) include(python) # Internal helper function for checking that a YAML context has been created # before operating on it. # Will result in CMake error if context does not exist. function(internal_yaml_context_required) cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) if(NOT result) message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' does not exist." "Remember to create a YAML context using 'yaml_create()' or 'yaml_load()'" ) endif() endfunction() # Internal helper function for checking if a YAML context is free before creating # it later. # Will result in CMake error if context exists. function(internal_yaml_context_free) cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) if(result) message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' already exists." "Please create a YAML context with a unique name" ) endif() endfunction() # Internal helper function to provide the correct initializer for a list in the # JSON content. function(internal_yaml_list_initializer var genex) if(genex) set(${var} "\"@YAML-LIST@\"" PARENT_SCOPE) else() set(${var} "[]" PARENT_SCOPE) endif() endfunction() # Internal helper function to append items to a list in the JSON content. # Unassigned arguments are the values to be appended. function(internal_yaml_list_append var genex key) set(json_content "${${var}}") string(JSON subjson GET "${json_content}" ${key}) if(genex) # new lists are stored in CMake string format, but those imported via # yaml_load() are proper JSON arrays. When an append is requested, those # must be converted back to a CMake list. string(JSON type TYPE "${json_content}" ${key}) if(type STREQUAL ARRAY) string(JSON arraylength LENGTH "${subjson}") internal_yaml_list_initializer(subjson TRUE) if(${arraylength} GREATER 0) math(EXPR arraystop "${arraylength} - 1") foreach(i RANGE 0 ${arraystop}) string(JSON item GET "${json_content}" ${key} ${i}) list(APPEND subjson ${item}) endforeach() endif() endif() list(APPEND subjson ${ARGN}) string(JSON json_content SET "${json_content}" ${key} "\"${subjson}\"") else() # lists are stored as JSON arrays string(JSON index LENGTH "${subjson}") list(LENGTH ARGN length) if(NOT length EQUAL 0) list(GET ARG_YAML_LIST 0 entry_0) if(entry_0 STREQUAL MAP) math(EXPR length "${length} / 2") math(EXPR stop "${index} + ${length} - 1") foreach(i RANGE ${index} ${stop}) list(POP_FRONT ARG_YAML_LIST argument) if(NOT argument STREQUAL MAP) message(FATAL_ERROR "yaml_set(${argument} ) is not valid at this position.\n" "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" ) endif() list(POP_FRONT ARG_YAML_LIST map_value) string(REGEX REPLACE "([^\\])," "\\1;" pair_list "${map_value}") set(quoted_map_value) foreach(pair ${pair_list}) if(NOT pair MATCHES "[^ ]*:[^ ]*") message(FATAL_ERROR "yaml_set(MAP ${map_value} ) is malformed.\n" "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"\n" "If value contains comma ',' then ensure the value field is properly quoted " "and escaped" ) endif() string(REGEX MATCH "^[^:]*" map_key "${pair}") string(REGEX REPLACE "^${map_key}:[ ]*" "" value "${pair}") string(STRIP "${map_key}" map_key) if(value MATCHES "," AND NOT (value MATCHES "\\\\," AND value MATCHES "'.*'")) message(FATAL_ERROR "value: ${value} is not properly quoted") endif() string(REGEX REPLACE "\\\\," "," value "${value}") list(APPEND quoted_map_value "\"${map_key}\": \"${value}\"") endforeach() list(JOIN quoted_map_value "," quoted_map_value) string(JSON json_content SET "${json_content}" ${key} ${i} "{${quoted_map_value}}") endforeach() else() math(EXPR stop "${index} + ${length} - 1") list(GET ARG_YAML_LIST 0 entry_0) foreach(i RANGE ${index} ${stop}) list(POP_FRONT ARGN value) string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"") endforeach() endif() endif() endif() set(${var} "${json_content}" PARENT_SCOPE) endfunction() # Usage # yaml_context(EXISTS NAME ) # # Function to query the status of the YAML context with the name . # The result of the query is stored in # # EXISTS : Check if the YAML context exists in the current scope # If the context exists, then TRUE is returned in # NAME : Name of the YAML context # : Variable to store the result of the query. # function(yaml_context) cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME) if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Missing argument in " "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} )." ) endif() zephyr_scope_exists(scope_defined ${ARG_YAML_NAME}) if(scope_defined) list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var) set(${out-var} TRUE PARENT_SCOPE) else() set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE) endif() endfunction() # Usage: # yaml_create(NAME [FILE ]) # # Create a new empty YAML context. # Use the file for storing the context when 'yaml_save(NAME )' is # called. # # Values can be set by calling 'yaml_set(NAME )' by using the # specified when creating the YAML context. # # NAME : Name of the YAML context. # FILE : Path to file to be used together with this YAML context. # function(yaml_create) cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) internal_yaml_context_free(NAME ${ARG_YAML_NAME}) zephyr_create_scope(${ARG_YAML_NAME}) if(DEFINED ARG_YAML_FILE) zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) endif() zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME}) endfunction() # Usage: # yaml_load(FILE NAME ) # # Load an existing YAML file and store its content in the YAML context . # # Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using # the same YAML scope name. # # FILE : Path to file to load. # NAME : Name of the YAML context. # function(yaml_load) cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME) internal_yaml_context_free(NAME ${ARG_YAML_NAME}) zephyr_create_scope(${ARG_YAML_NAME}) zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}')) or {}))" OUTPUT_VARIABLE json_load_out ERROR_VARIABLE json_load_error RESULT_VARIABLE json_load_result ) if(json_load_result) message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n" "${json_load_error}" ) endif() zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME}) endfunction() # Usage: # yaml_get( NAME KEY ...) # # Get the value of the given key and store the value in . # If key represents a list, then the list is returned. # # Behavior is undefined if key points to a complex object. # # NAME : Name of the YAML context. # KEY ... : Name of key. # : Name of output variable. # function(yaml_get out_var) # Current limitation: # - Anything will be returned, even json object strings. cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) internal_yaml_context_required(NAME ${ARG_YAML_NAME}) zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) # We specify error variable to avoid a fatal error. # If key is not found, then type becomes '-NOTFOUND' and value handling is done below. string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) if(type STREQUAL ARRAY) string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) string(JSON arraylength LENGTH "${subjson}") set(array) math(EXPR arraystop "${arraylength} - 1") if(arraylength GREATER 0) foreach(i RANGE 0 ${arraystop}) string(JSON item GET "${subjson}" ${i}) list(APPEND array ${item}) endforeach() endif() set(${out_var} ${array} PARENT_SCOPE) else() # We specify error variable to avoid a fatal error. # Searching for a non-existing key should just result in the output value '-NOTFOUND' string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY}) set(${out_var} ${value} PARENT_SCOPE) endif() endfunction() # Usage: # yaml_length( NAME KEY ...) # # Get the length of the array defined by the given key and store the length in . # If key does not define an array, then the length -1 is returned. # # NAME : Name of the YAML context. # KEY ... : Name of key defining the list. # : Name of output variable. # function(yaml_length out_var) cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) internal_yaml_context_required(NAME ${ARG_YAML_NAME}) zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) if(type STREQUAL ARRAY) string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) string(JSON arraylength LENGTH "${subjson}") set(${out_var} ${arraylength} PARENT_SCOPE) elseif(type MATCHES ".*-NOTFOUND") set(${out_var} ${type} PARENT_SCOPE) else() message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.") set(${out_var} -1 PARENT_SCOPE) endif() endfunction() # Usage: # yaml_set(NAME KEY ... [GENEX] VALUE ) # yaml_set(NAME KEY ... [APPEND] [GENEX] LIST ...) # yaml_set(NAME KEY ... [APPEND] LIST MAP MAP MAP ...) # # Set a value or a list of values to given key. # # If setting a list of values, then APPEND can be specified to indicate that the # list of values should be appended to the existing list identified with key(s). # # NAME : Name of the YAML context. # KEY ... : Name of key. # VALUE : New value for the key. # LIST : New list of values for the key. # APPEND : Append the list of values to the list of values for the key. # GENEX : The value(s) contain generator expressions. When using this # option, also see the notes in the yaml_save() function. # MAP : Map, with key-value pairs where key-value is separated by ':', # and pairs separated by ','. # Format example: ": , : , ..." # MAP can be given multiple times to separate maps when adding them to a list. # LIST MAP cannot be used with GENEX. # # Note: if a map value contains commas, ',', then the value string must be quoted in # single quotes and commas must be double escaped, like this: 'A \\,string' # function(yaml_set) cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) internal_yaml_context_required(NAME ${ARG_YAML_NAME}) if(ARG_YAML_GENEX) zephyr_set(GENEX TRUE SCOPE ${ARG_YAML_NAME}) endif() if(DEFINED ARG_YAML_LIST OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES) set(key_is_list TRUE) endif() if(ARG_YAML_APPEND AND NOT key_is_list) message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}(APPEND ...) can only be used with argument: LIST") endif() if(ARG_YAML_GENEX AND MAP IN_LIST ARG_YAML_LIST) message(FATAL_ERROR "${function}(GENEX ...) cannot be used with argument: LIST MAP") endif() zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) set(yaml_key_undefined ${ARG_YAML_KEY}) foreach(k ${yaml_key_undefined}) list(REMOVE_AT yaml_key_undefined 0) # We ignore any errors as we are checking for existence of the key, and # non-existing keys will throw errors but also set type to NOT-FOUND. string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k}) if(NOT type) list(APPEND yaml_key_create ${k}) break() endif() list(APPEND valid_keys ${k}) endforeach() list(REVERSE yaml_key_undefined) if(NOT "${yaml_key_undefined}" STREQUAL "") if(key_is_list) internal_yaml_list_initializer(json_string ${genex}) else() set(json_string "\"\"") endif() foreach(k ${yaml_key_undefined}) set(json_string "{\"${k}\": ${json_string}}") endforeach() string(JSON json_content SET "${json_content}" ${valid_keys} ${yaml_key_create} "${json_string}" ) endif() if(key_is_list) if(NOT ARG_YAML_APPEND) internal_yaml_list_initializer(json_string ${genex}) string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}") endif() zephyr_string(ESCAPE escape_list "${ARG_YAML_LIST}") internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${escape_list}) else() zephyr_string(ESCAPE escape_value "${ARG_YAML_VALUE}") string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${escape_value}\"") endif() zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) endfunction() # Usage: # yaml_remove(NAME KEY ...) # # Remove the KEY ... from the YAML context . # # Several levels of keys can be given, for example: # KEY build cmake command # # To remove the key 'command' underneath 'cmake' in the toplevel 'build' # # NAME : Name of the YAML context. # KEY : Name of key to remove. # function(yaml_remove) cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) internal_yaml_context_required(NAME ${ARG_YAML_NAME}) zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY}) zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) endfunction() # Usage: # yaml_save(NAME [FILE ]) # # Write the YAML context to , or the one given with the earlier # 'yaml_load()' or 'yaml_create()' call. This will be performed immediately if # the context does not use generator expressions; otherwise, keys that include # a generator expression will initially be written as comments, and the full # contents will be available at build time. Build steps that depend on the file # being complete must depend on the '_yaml_saved' target. # # NAME : Name of the YAML context # FILE : Path to file to write the context. # If not given, then the FILE property of the YAML context will be # used. In case both FILE is omitted and FILE property is missing # on the YAML context, then an error will be raised. # function(yaml_save) cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN}) zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) internal_yaml_context_required(NAME ${ARG_YAML_NAME}) zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) if(NOT yaml_file) zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE) endif() if(DEFINED ARG_YAML_FILE) set(yaml_file ${ARG_YAML_FILE}) else() zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) endif() zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) if(genex) to_yaml("${json_content}" 0 yaml_out DIRECT_GENEX) else() to_yaml("${json_content}" 0 yaml_out DIRECT) endif() if(EXISTS ${yaml_file}) FILE(RENAME ${yaml_file} ${yaml_file}.bak) endif() FILE(WRITE ${yaml_file} "${yaml_out}") set(save_target ${ARG_YAML_NAME}_yaml_saved) if(NOT TARGET ${save_target}) # Create a target for the completion of the YAML save operation. # This will be a dummy unless genexes are used. add_custom_target(${save_target} ALL DEPENDS ${yaml_file}) set_target_properties(${save_target} PROPERTIES genex_save_count 0 temp_files "" ) endif() if(genex) get_property(genex_save_count TARGET ${save_target} PROPERTY genex_save_count) if(${genex_save_count} EQUAL 0) # First yaml_save() for this context with genexes enabled add_custom_command( OUTPUT ${yaml_file} DEPENDS $ COMMAND ${CMAKE_COMMAND} -DEXPANDED_FILE="$" -DOUTPUT_FILE="${yaml_file}" -DTEMP_FILES="$" -P ${ZEPHYR_BASE}/cmake/yaml-filter.cmake ) endif() math(EXPR genex_save_count "${genex_save_count} + 1") set_property(TARGET ${save_target} PROPERTY genex_save_count ${genex_save_count}) cmake_path(SET yaml_path "${yaml_file}") cmake_path(GET yaml_path STEM yaml_file_no_ext) set(expanded_file ${CMAKE_CURRENT_BINARY_DIR}/${yaml_file_no_ext}_${genex_save_count}.yaml) set_property(TARGET ${save_target} PROPERTY expanded_file ${expanded_file}) # comment this to keep the temporary files set_property(TARGET ${save_target} APPEND PROPERTY temp_files ${expanded_file}) to_yaml("${json_content}" 0 yaml_out TEMP_GENEX) FILE(GENERATE OUTPUT ${expanded_file} CONTENT "${yaml_out}") FILE(TOUCH ${expanded_file}) # ensure timestamp is updated even if nothing changed endif() endfunction() function(to_yaml json level yaml mode) if(mode STREQUAL "DIRECT") # Direct output mode, no genexes: write a standard YAML set(expand_lists TRUE) set(escape_quotes TRUE) set(comment_genexes FALSE) elseif(mode STREQUAL "DIRECT_GENEX" OR mode STREQUAL "FINAL_GENEX") # Direct output mode with genexes enabled, or final write of post-processed # file: write a standard YAML, comment entries with genexes if they are # (still) present in the file set(expand_lists TRUE) set(escape_quotes TRUE) set(comment_genexes TRUE) elseif(mode STREQUAL "TEMP_GENEX") # Temporary output mode for genex expansion: save single quotes with no # special processing, since they will be fixed up by yaml-filter.cmake set(expand_lists FALSE) set(escape_quotes FALSE) set(comment_genexes FALSE) else() message(FATAL_ERROR "to_yaml(... ${mode} ) is malformed.") endif() if(level EQUAL 0) # Top-level call, initialize the YAML output variable set(${yaml} "" PARENT_SCOPE) else() math(EXPR level_dec "${level} - 1") set(indent_${level} "${indent_${level_dec}} ") endif() string(JSON length LENGTH "${json}") if(length EQUAL 0) # Empty object return() endif() math(EXPR stop "${length} - 1") foreach(i RANGE 0 ${stop}) string(JSON member MEMBER "${json}" ${i}) string(JSON type TYPE "${json}" ${member}) string(JSON subjson GET "${json}" ${member}) if(type STREQUAL OBJECT) # JSON object -> YAML dictionary set(${yaml} "${${yaml}}${indent_${level}}${member}:\n") math(EXPR sublevel "${level} + 1") to_yaml("${subjson}" ${sublevel} ${yaml} ${mode}) elseif(type STREQUAL ARRAY) # JSON array -> YAML list set(${yaml} "${${yaml}}${indent_${level}}${member}:") string(JSON arraylength LENGTH "${subjson}") if(${arraylength} LESS 1) set(${yaml} "${${yaml}} []\n") else() set(${yaml} "${${yaml}}\n") math(EXPR arraystop "${arraylength} - 1") foreach(i RANGE 0 ${arraystop}) string(JSON item GET "${json}" ${member} ${i}) # Check the length of item. Only OBJECT and ARRAY may have length, so a length at this # level means `to_yaml()` should be called recursively. string(JSON length ERROR_VARIABLE ignore LENGTH "${item}") if(length) set(non_indent_yaml) to_yaml("${item}" 0 non_indent_yaml ${mode}) string(REGEX REPLACE "\n$" "" non_indent_yaml "${non_indent_yaml}") string(REPLACE "\n" "\n${indent_${level}} " indent_yaml "${non_indent_yaml}") set(${yaml} "${${yaml}}${indent_${level}} - ${indent_yaml}\n") else() # Assume a string, escape single quotes when required (see comment below). if(escape_quotes) string(REPLACE "'" "''" item "${item}") endif() set(${yaml} "${${yaml}}${indent_${level}} - '${item}'\n") endif() endforeach() endif() elseif(type STREQUAL STRING) # JSON string maps to multiple YAML types: # - with unexpanded generator expressions: save as YAML comment # - if it matches the special prefix: convert to YAML list # - otherwise: save as YAML scalar # Single quotes must be escaped in the value _unless_ this will be used # to expand generator expressions, because then the escaping will be # addressed once in the yaml-filter.cmake script. if(escape_quotes) string(REPLACE "'" "''" subjson "${subjson}") endif() if(subjson MATCHES "\\$<.*>" AND comment_genexes) # Yet unexpanded generator expression: save as comment string(SUBSTRING ${indent_${level}} 1 -1 short_indent) set(${yaml} "${${yaml}}#${short_indent}${member}: '${subjson}'\n") elseif(subjson MATCHES "^@YAML-LIST@" AND expand_lists) # List-as-string: convert to list set(${yaml} "${${yaml}}${indent_${level}}${member}:") list(POP_FRONT subjson) if(subjson STREQUAL "") set(${yaml} "${${yaml}} []\n") else() set(${yaml} "${${yaml}}\n") foreach(item ${subjson}) set(${yaml} "${${yaml}}${indent_${level}} - '${item}'\n") endforeach() endif() else() # Raw strings: save as is set(${yaml} "${${yaml}}${indent_${level}}${member}: '${subjson}'\n") endif() else() # Other JSON data type -> YAML scalar, as-is set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n") endif() endforeach() set(${yaml} ${${yaml}} PARENT_SCOPE) endfunction()