1# SPDX-License-Identifier: Apache-2.0 2# 3# Copyright (c) 2024, Nordic Semiconductor ASA 4 5# CMake YAML module for handling of YAML files. 6# 7# This module offers basic support for simple yaml files. 8# 9# It supports basic key-value pairs, like 10# foo: bar 11# 12# basic key-object pairs, like 13# foo: 14# bar: baz 15# 16# Simple value lists, like: 17# foos: 18# - foo1 19# - foo2 20# - foo3 21# 22# Support for list of maps, like: 23# foo: 24# - bar: val1 25# baz: val1 26# - bar: val2 27# baz: val2 28# 29# All of above can be combined, for example like: 30# foo: 31# bar: baz 32# quz: 33# greek: 34# - alpha 35# - beta 36# - gamma 37# fred: thud 38 39include_guard(GLOBAL) 40 41include(extensions) 42include(python) 43 44# Internal helper function for checking that a YAML context has been created 45# before operating on it. 46# Will result in CMake error if context does not exist. 47function(internal_yaml_context_required) 48 cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) 49 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 50 yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) 51 52 if(NOT result) 53 message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' does not exist." 54 "Remember to create a YAML context using 'yaml_create()' or 'yaml_load()'" 55 ) 56 endif() 57endfunction() 58 59# Internal helper function for checking if a YAML context is free before creating 60# it later. 61# Will result in CMake error if context exists. 62function(internal_yaml_context_free) 63 cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) 64 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 65 yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) 66 67 if(result) 68 message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' already exists." 69 "Please create a YAML context with a unique name" 70 ) 71 endif() 72endfunction() 73 74# Internal helper function to provide the correct initializer for a list in the 75# JSON content. 76function(internal_yaml_list_initializer var genex) 77 if(genex) 78 set(${var} "\"@YAML-LIST@\"" PARENT_SCOPE) 79 else() 80 set(${var} "[]" PARENT_SCOPE) 81 endif() 82endfunction() 83 84# Internal helper function to append items to a list in the JSON content. 85# Unassigned arguments are the values to be appended. 86function(internal_yaml_list_append var genex key) 87 set(json_content "${${var}}") 88 string(JSON subjson GET "${json_content}" ${key}) 89 if(genex) 90 # new lists are stored in CMake string format, but those imported via 91 # yaml_load() are proper JSON arrays. When an append is requested, those 92 # must be converted back to a CMake list. 93 string(JSON type TYPE "${json_content}" ${key}) 94 if(type STREQUAL ARRAY) 95 string(JSON arraylength LENGTH "${subjson}") 96 internal_yaml_list_initializer(subjson TRUE) 97 if(${arraylength} GREATER 0) 98 math(EXPR arraystop "${arraylength} - 1") 99 list(GET ARG_YAML_LIST 0 entry_0) 100 if(entry_0 STREQUAL MAP) 101 message(FATAL_ERROR "${function}(GENEX ${argument} ) is not valid at this position.\n" 102 "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" 103 ) 104 endif() 105 106 foreach(i RANGE 0 ${arraystop}) 107 string(JSON item GET "${json_content}" ${key} ${i}) 108 list(APPEND subjson ${item}) 109 endforeach() 110 endif() 111 endif() 112 list(APPEND subjson ${ARGN}) 113 string(JSON json_content SET "${json_content}" ${key} "\"${subjson}\"") 114 else() 115 # lists are stored as JSON arrays 116 string(JSON index LENGTH "${subjson}") 117 list(LENGTH ARGN length) 118 if(NOT length EQUAL 0) 119 list(GET ARG_YAML_LIST 0 entry_0) 120 if(entry_0 STREQUAL MAP) 121 math(EXPR length "${length} / 2") 122 math(EXPR stop "${index} + ${length} - 1") 123 foreach(i RANGE ${index} ${stop}) 124 list(POP_FRONT ARG_YAML_LIST argument) 125 if(NOT argument STREQUAL MAP) 126 message(FATAL_ERROR "yaml_set(${argument} ) is not valid at this position.\n" 127 "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" 128 ) 129 endif() 130 list(POP_FRONT ARG_YAML_LIST map_value) 131 string(REGEX REPLACE "([^\\])," "\\1;" pair_list "${map_value}") 132 set(qouted_map_value) 133 foreach(pair ${pair_list}) 134 if(NOT pair MATCHES "[^ ]*:[^ ]*") 135 message(FATAL_ERROR "yaml_set(MAP ${map_value} ) is malformed.\n" 136 "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"\n" 137 "If value contains comma ',' then ensure the value field is properly qouted " 138 "and escaped" 139 ) 140 endif() 141 string(REGEX MATCH "^[^:]*" map_key "${pair}") 142 string(REGEX REPLACE "^${map_key}:[ ]*" "" value "${pair}") 143 string(STRIP "${map_key}" map_key) 144 if(value MATCHES "," AND NOT (value MATCHES "\\\\," AND value MATCHES "'.*'")) 145 message(FATAL_ERROR "value: ${value} is not properly quoted") 146 endif() 147 string(REGEX REPLACE "\\\\," "," value "${value}") 148 list(APPEND qouted_map_value "\"${map_key}\": \"${value}\"") 149 endforeach() 150 list(JOIN qouted_map_value "," qouted_map_value) 151 string(JSON json_content SET "${json_content}" ${key} ${i} "{${qouted_map_value}}") 152 endforeach() 153 else() 154 math(EXPR stop "${index} + ${length} - 1") 155 list(GET ARG_YAML_LIST 0 entry_0) 156 foreach(i RANGE ${index} ${stop}) 157 list(POP_FRONT ARGN value) 158 string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"") 159 endforeach() 160 endif() 161 endif() 162 endif() 163 set(${var} "${json_content}" PARENT_SCOPE) 164endfunction() 165 166# Usage 167# yaml_context(EXISTS NAME <name> <result>) 168# 169# Function to query the status of the YAML context with the name <name>. 170# The result of the query is stored in <result> 171# 172# EXISTS : Check if the YAML context exists in the current scope 173# If the context exists, then TRUE is returned in <result> 174# NAME <name>: Name of the YAML context 175# <result> : Variable to store the result of the query. 176# 177function(yaml_context) 178 cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN}) 179 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME) 180 181 if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS) 182 message(FATAL_ERROR "Missing argument in " 183 "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} <result-var>)." 184 ) 185 endif() 186 187 zephyr_scope_exists(scope_defined ${ARG_YAML_NAME}) 188 if(scope_defined) 189 list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var) 190 set(${out-var} TRUE PARENT_SCOPE) 191 else() 192 set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE) 193 endif() 194endfunction() 195 196# Usage: 197# yaml_create(NAME <name> [FILE <file>]) 198# 199# Create a new empty YAML context. 200# Use the file <file> for storing the context when 'yaml_save(NAME <name>)' is 201# called. 202# 203# Values can be set by calling 'yaml_set(NAME <name>)' by using the <name> 204# specified when creating the YAML context. 205# 206# NAME <name>: Name of the YAML context. 207# FILE <file>: Path to file to be used together with this YAML context. 208# 209function(yaml_create) 210 cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) 211 212 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 213 214 internal_yaml_context_free(NAME ${ARG_YAML_NAME}) 215 zephyr_create_scope(${ARG_YAML_NAME}) 216 if(DEFINED ARG_YAML_FILE) 217 zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) 218 endif() 219 zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) 220 zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME}) 221endfunction() 222 223# Usage: 224# yaml_load(FILE <file> NAME <name>) 225# 226# Load an existing YAML file and store its content in the YAML context <name>. 227# 228# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using 229# the same YAML scope name. 230# 231# FILE <file>: Path to file to load. 232# NAME <name>: Name of the YAML context. 233# 234function(yaml_load) 235 cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) 236 237 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME) 238 internal_yaml_context_free(NAME ${ARG_YAML_NAME}) 239 240 zephyr_create_scope(${ARG_YAML_NAME}) 241 zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) 242 243 execute_process(COMMAND ${PYTHON_EXECUTABLE} -c 244 "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}')) or {}))" 245 OUTPUT_VARIABLE json_load_out 246 ERROR_VARIABLE json_load_error 247 RESULT_VARIABLE json_load_result 248 ) 249 250 if(json_load_result) 251 message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n" 252 "${json_load_error}" 253 ) 254 endif() 255 256 zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) 257 zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME}) 258endfunction() 259 260# Usage: 261# yaml_get(<out-var> NAME <name> KEY <key>...) 262# 263# Get the value of the given key and store the value in <out-var>. 264# If key represents a list, then the list is returned. 265# 266# Behavior is undefined if key points to a complex object. 267# 268# NAME <name> : Name of the YAML context. 269# KEY <key>... : Name of key. 270# <out-var> : Name of output variable. 271# 272function(yaml_get out_var) 273 # Current limitation: 274 # - Anything will be returned, even json object strings. 275 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 276 277 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 278 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 279 280 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 281 282 # We specify error variable to avoid a fatal error. 283 # If key is not found, then type becomes '-NOTFOUND' and value handling is done below. 284 string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) 285 if(type STREQUAL ARRAY) 286 string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) 287 string(JSON arraylength LENGTH "${subjson}") 288 set(array) 289 math(EXPR arraystop "${arraylength} - 1") 290 if(arraylength GREATER 0) 291 foreach(i RANGE 0 ${arraystop}) 292 string(JSON item GET "${subjson}" ${i}) 293 list(APPEND array ${item}) 294 endforeach() 295 endif() 296 set(${out_var} ${array} PARENT_SCOPE) 297 else() 298 # We specify error variable to avoid a fatal error. 299 # Searching for a non-existing key should just result in the output value '-NOTFOUND' 300 string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY}) 301 set(${out_var} ${value} PARENT_SCOPE) 302 endif() 303endfunction() 304 305# Usage: 306# yaml_length(<out-var> NAME <name> KEY <key>...) 307# 308# Get the length of the array defined by the given key and store the length in <out-var>. 309# If key does not define an array, then the length -1 is returned. 310# 311# NAME <name> : Name of the YAML context. 312# KEY <key>... : Name of key defining the list. 313# <out-var> : Name of output variable. 314# 315function(yaml_length out_var) 316 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 317 318 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 319 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 320 321 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 322 323 string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) 324 if(type STREQUAL ARRAY) 325 string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) 326 string(JSON arraylength LENGTH "${subjson}") 327 set(${out_var} ${arraylength} PARENT_SCOPE) 328 elseif(type MATCHES ".*-NOTFOUND") 329 set(${out_var} ${type} PARENT_SCOPE) 330 else() 331 message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.") 332 set(${out_var} -1 PARENT_SCOPE) 333 endif() 334endfunction() 335 336# Usage: 337# yaml_set(NAME <name> KEY <key>... [GENEX] VALUE <value>) 338# yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...) 339# yaml_set(NAME <name> KEY <key>... [APPEND] LIST MAP <map1> MAP <map2> MAP ...) 340# 341# Set a value or a list of values to given key. 342# 343# If setting a list of values, then APPEND can be specified to indicate that the 344# list of values should be appended to the existing list identified with key(s). 345# 346# NAME <name> : Name of the YAML context. 347# KEY <key>... : Name of key. 348# VALUE <value>: New value for the key. 349# LIST <values>: New list of values for the key. 350# APPEND : Append the list of values to the list of values for the key. 351# GENEX : The value(s) contain generator expressions. When using this 352# option, also see the notes in the yaml_save() function. 353# MAP <map> : Map, with key-value pairs where key-value is separated by ':', 354# and pairs separated by ','. 355# Format example: "<key1>: <value1>, <key2>: <value2>, ..." 356# MAP can be given multiple times to separate maps when adding them to a list. 357# LIST MAP cannot be used with GENEX. 358# 359# Note: if a map value contains commas, ',', then the value string must be quoted in 360# single quotes and commas must be double escaped, like this: 'A \\,string' 361# 362function(yaml_set) 363 cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN}) 364 365 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 366 zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) 367 zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) 368 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 369 370 if(ARG_YAML_GENEX) 371 zephyr_set(GENEX TRUE SCOPE ${ARG_YAML_NAME}) 372 endif() 373 374 if(DEFINED ARG_YAML_LIST 375 OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES) 376 set(key_is_list TRUE) 377 endif() 378 379 if(ARG_YAML_APPEND AND NOT key_is_list) 380 message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}(APPEND ...) can only be used with argument: LIST") 381 endif() 382 383 if(ARG_YAML_GENEX AND MAP IN_LIST ARG_YAML_LIST) 384 message(FATAL_ERROR "${function}(GENEX ...) cannot be used with argument: LIST MAP") 385 endif() 386 387 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 388 zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) 389 390 set(yaml_key_undefined ${ARG_YAML_KEY}) 391 foreach(k ${yaml_key_undefined}) 392 list(REMOVE_AT yaml_key_undefined 0) 393 # We ignore any errors as we are checking for existence of the key, and 394 # non-existing keys will throw errors but also set type to NOT-FOUND. 395 string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k}) 396 397 if(NOT type) 398 list(APPEND yaml_key_create ${k}) 399 break() 400 endif() 401 list(APPEND valid_keys ${k}) 402 endforeach() 403 404 list(REVERSE yaml_key_undefined) 405 if(NOT "${yaml_key_undefined}" STREQUAL "") 406 if(key_is_list) 407 internal_yaml_list_initializer(json_string ${genex}) 408 else() 409 set(json_string "\"\"") 410 endif() 411 412 foreach(k ${yaml_key_undefined}) 413 set(json_string "{\"${k}\": ${json_string}}") 414 endforeach() 415 string(JSON json_content SET "${json_content}" 416 ${valid_keys} ${yaml_key_create} "${json_string}" 417 ) 418 endif() 419 420 if(key_is_list) 421 if(NOT ARG_YAML_APPEND) 422 internal_yaml_list_initializer(json_string ${genex}) 423 string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}") 424 endif() 425 zephyr_string(ESCAPE escape_list "${ARG_YAML_LIST}") 426 internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${escape_list}) 427 else() 428 zephyr_string(ESCAPE escape_value "${ARG_YAML_VALUE}") 429 string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${escape_value}\"") 430 endif() 431 432 zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) 433endfunction() 434 435# Usage: 436# yaml_remove(NAME <name> KEY <key>...) 437# 438# Remove the KEY <key>... from the YAML context <name>. 439# 440# Several levels of keys can be given, for example: 441# KEY build cmake command 442# 443# To remove the key 'command' underneath 'cmake' in the toplevel 'build' 444# 445# NAME <name>: Name of the YAML context. 446# KEY <key> : Name of key to remove. 447# 448function(yaml_remove) 449 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 450 451 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 452 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 453 454 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 455 string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY}) 456 457 zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) 458endfunction() 459 460# Usage: 461# yaml_save(NAME <name> [FILE <file>]) 462# 463# Write the YAML context <name> to <file>, or the one given with the earlier 464# 'yaml_load()' or 'yaml_create()' call. This will be performed immediately if 465# the context does not use generator expressions; otherwise, keys that include 466# a generator expression will initially be written as comments, and the full 467# contents will be available at build time. Build steps that depend on the file 468# being complete must depend on the '<name>_yaml_saved' target. 469# 470# NAME <name>: Name of the YAML context 471# FILE <file>: Path to file to write the context. 472# If not given, then the FILE property of the YAML context will be 473# used. In case both FILE is omitted and FILE property is missing 474# on the YAML context, then an error will be raised. 475# 476function(yaml_save) 477 cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN}) 478 479 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 480 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 481 482 zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) 483 if(NOT yaml_file) 484 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE) 485 endif() 486 if(DEFINED ARG_YAML_FILE) 487 set(yaml_file ${ARG_YAML_FILE}) 488 else() 489 zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) 490 endif() 491 492 zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) 493 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 494 to_yaml("${json_content}" 0 yaml_out ${genex}) 495 496 if(EXISTS ${yaml_file}) 497 FILE(RENAME ${yaml_file} ${yaml_file}.bak) 498 endif() 499 FILE(WRITE ${yaml_file} "${yaml_out}") 500 501 set(save_target ${ARG_YAML_NAME}_yaml_saved) 502 if (NOT TARGET ${save_target}) 503 # Create a target for the completion of the YAML save operation. 504 # This will be a dummy unless genexes are used. 505 add_custom_target(${save_target} ALL DEPENDS ${yaml_file}) 506 set_target_properties(${save_target} PROPERTIES 507 genex_save_count 0 508 temp_files "" 509 ) 510 endif() 511 512 if (genex) 513 get_property(genex_save_count TARGET ${save_target} PROPERTY genex_save_count) 514 if (${genex_save_count} EQUAL 0) 515 # First yaml_save() for this context with genexes enabled 516 add_custom_command( 517 OUTPUT ${yaml_file} 518 DEPENDS $<TARGET_PROPERTY:${save_target},json_file> 519 COMMAND ${CMAKE_COMMAND} 520 -DJSON_FILE="$<TARGET_PROPERTY:${save_target},json_file>" 521 -DYAML_FILE="${yaml_file}" 522 -DTEMP_FILES="$<TARGET_PROPERTY:${save_target},temp_files>" 523 -P ${ZEPHYR_BASE}/cmake/yaml-filter.cmake 524 ) 525 endif() 526 527 math(EXPR genex_save_count "${genex_save_count} + 1") 528 set_property(TARGET ${save_target} PROPERTY genex_save_count ${genex_save_count}) 529 530 cmake_path(SET yaml_path "${yaml_file}") 531 cmake_path(GET yaml_path STEM yaml_file_no_ext) 532 set(json_file ${yaml_file_no_ext}_${genex_save_count}.json) 533 set_property(TARGET ${save_target} PROPERTY json_file ${json_file}) 534 535 # comment this to keep the temporary JSON files 536 set_property(TARGET ${save_target} APPEND PROPERTY temp_files ${json_file}) 537 538 FILE(GENERATE OUTPUT ${json_file} 539 CONTENT "${json_content}" 540 ) 541 endif() 542endfunction() 543 544function(to_yaml in_json level yaml genex) 545 zephyr_string(ESCAPE json "${in_json}") 546 if(level GREATER 0) 547 math(EXPR level_dec "${level} - 1") 548 set(indent_${level} "${indent_${level_dec}} ") 549 endif() 550 551 string(JSON length LENGTH "${json}") 552 if(length EQUAL 0) 553 # Empty object 554 return() 555 endif() 556 557 math(EXPR stop "${length} - 1") 558 foreach(i RANGE 0 ${stop}) 559 string(JSON member MEMBER "${json}" ${i}) 560 561 string(JSON type TYPE "${json}" ${member}) 562 string(JSON subjson GET "${json}" ${member}) 563 if(type STREQUAL OBJECT) 564 # JSON object -> YAML dictionary 565 set(${yaml} "${${yaml}}${indent_${level}}${member}:\n") 566 math(EXPR sublevel "${level} + 1") 567 to_yaml("${subjson}" ${sublevel} ${yaml} ${genex}) 568 elseif(type STREQUAL ARRAY) 569 # JSON array -> YAML list 570 set(${yaml} "${${yaml}}${indent_${level}}${member}:") 571 string(JSON arraylength LENGTH "${subjson}") 572 if(${arraylength} LESS 1) 573 set(${yaml} "${${yaml}} []\n") 574 else() 575 set(${yaml} "${${yaml}}\n") 576 math(EXPR arraystop "${arraylength} - 1") 577 foreach(i RANGE 0 ${arraystop}) 578 string(JSON item GET "${json}" ${member} ${i}) 579 # Check the length of item. Only OBJECT and ARRAY may have length, so a length at this 580 # level means `to_yaml()` should be called recursively. 581 string(JSON length ERROR_VARIABLE ignore LENGTH "${item}") 582 if(length) 583 set(non_indent_yaml) 584 to_yaml("${item}" 0 non_indent_yaml FALSE) 585 string(REGEX REPLACE "\n$" "" non_indent_yaml "${non_indent_yaml}") 586 string(REPLACE "\n" "\n${indent_${level}} " indent_yaml "${non_indent_yaml}") 587 set(${yaml} "${${yaml}}${indent_${level}} - ${indent_yaml}\n") 588 else() 589 set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n") 590 endif() 591 endforeach() 592 endif() 593 elseif(type STREQUAL STRING) 594 # JSON string maps to multiple YAML types: 595 # - with unexpanded generator expressions: save as YAML comment 596 # - if it matches the special prefix: convert to YAML list 597 # - otherwise: save as YAML scalar 598 if (subjson MATCHES "\\$<.*>" AND ${genex}) 599 # Yet unexpanded generator expression: save as comment 600 string(SUBSTRING ${indent_${level}} 1 -1 short_indent) 601 set(${yaml} "${${yaml}}#${short_indent}${member}: ${subjson}\n") 602 elseif(subjson MATCHES "^@YAML-LIST@") 603 # List-as-string: convert to list 604 set(${yaml} "${${yaml}}${indent_${level}}${member}:") 605 list(POP_FRONT subjson) 606 if(subjson STREQUAL "") 607 set(${yaml} "${${yaml}} []\n") 608 else() 609 set(${yaml} "${${yaml}}\n") 610 foreach(item ${subjson}) 611 set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n") 612 endforeach() 613 endif() 614 else() 615 # Raw strings: save as is 616 set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n") 617 endif() 618 else() 619 # Other JSON data type -> YAML scalar, as-is 620 set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n") 621 endif() 622 endforeach() 623 624 set(${yaml} ${${yaml}} PARENT_SCOPE) 625endfunction() 626