zephyr/cmake/modules/yaml.cmake
Luca Burelli dbf00bfca8 yaml: save intermediate files in the build directory
Make sure to provide full paths when saving the intermediate files in
the YAML export, to prevent them from being saved in the source tree.

Signed-off-by: Luca Burelli <l.burelli@arduino.cc>
2025-05-08 19:46:18 +02:00

657 lines
24 KiB
CMake

# 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 <name> <result>)
#
# Function to query the status of the YAML context with the name <name>.
# The result of the query is stored in <result>
#
# EXISTS : Check if the YAML context exists in the current scope
# If the context exists, then TRUE is returned in <result>
# NAME <name>: Name of the YAML context
# <result> : 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} <result-var>)."
)
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 <name> [FILE <file>])
#
# Create a new empty YAML context.
# Use the file <file> for storing the context when 'yaml_save(NAME <name>)' is
# called.
#
# Values can be set by calling 'yaml_set(NAME <name>)' by using the <name>
# specified when creating the YAML context.
#
# NAME <name>: Name of the YAML context.
# FILE <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 <file> NAME <name>)
#
# Load an existing YAML file and store its content in the YAML context <name>.
#
# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using
# the same YAML scope name.
#
# FILE <file>: Path to file to load.
# NAME <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(<out-var> NAME <name> KEY <key>...)
#
# Get the value of the given key and store the value in <out-var>.
# If key represents a list, then the list is returned.
#
# Behavior is undefined if key points to a complex object.
#
# NAME <name> : Name of the YAML context.
# KEY <key>... : Name of key.
# <out-var> : 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(<out-var> NAME <name> KEY <key>...)
#
# Get the length of the array defined by the given key and store the length in <out-var>.
# If key does not define an array, then the length -1 is returned.
#
# NAME <name> : Name of the YAML context.
# KEY <key>... : Name of key defining the list.
# <out-var> : 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 <name> KEY <key>... [GENEX] VALUE <value>)
# yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...)
# yaml_set(NAME <name> KEY <key>... [APPEND] LIST MAP <map1> MAP <map2> 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> : Name of the YAML context.
# KEY <key>... : Name of key.
# VALUE <value>: New value for the key.
# LIST <values>: 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> : Map, with key-value pairs where key-value is separated by ':',
# and pairs separated by ','.
# Format example: "<key1>: <value1>, <key2>: <value2>, ..."
# 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 <name> KEY <key>...)
#
# Remove the KEY <key>... from the YAML context <name>.
#
# 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>: Name of the YAML context.
# KEY <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 <name> [FILE <file>])
#
# Write the YAML context <name> to <file>, 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 '<name>_yaml_saved' target.
#
# NAME <name>: Name of the YAML context
# FILE <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 $<TARGET_PROPERTY:${save_target},expanded_file>
COMMAND ${CMAKE_COMMAND}
-DEXPANDED_FILE="$<TARGET_PROPERTY:${save_target},expanded_file>"
-DOUTPUT_FILE="${yaml_file}"
-DTEMP_FILES="$<TARGET_PROPERTY:${save_target},temp_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()