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 foreach(i RANGE 0 ${arraystop}) 100 string(JSON item GET "${json_content}" ${key} ${i}) 101 list(APPEND subjson ${item}) 102 endforeach() 103 endif() 104 endif() 105 list(APPEND subjson ${ARGN}) 106 string(JSON json_content SET "${json_content}" ${key} "\"${subjson}\"") 107 else() 108 # lists are stored as JSON arrays 109 string(JSON index LENGTH "${subjson}") 110 list(LENGTH ARGN length) 111 if(NOT length EQUAL 0) 112 list(GET ARG_YAML_LIST 0 entry_0) 113 if(entry_0 STREQUAL MAP) 114 math(EXPR length "${length} / 2") 115 math(EXPR stop "${index} + ${length} - 1") 116 foreach(i RANGE ${index} ${stop}) 117 list(POP_FRONT ARG_YAML_LIST argument) 118 if(NOT argument STREQUAL MAP) 119 message(FATAL_ERROR "yaml_set(${argument} ) is not valid at this position.\n" 120 "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" 121 ) 122 endif() 123 list(POP_FRONT ARG_YAML_LIST map_value) 124 string(REGEX REPLACE "([^\\])," "\\1;" pair_list "${map_value}") 125 set(quoted_map_value) 126 foreach(pair ${pair_list}) 127 if(NOT pair MATCHES "[^ ]*:[^ ]*") 128 message(FATAL_ERROR "yaml_set(MAP ${map_value} ) is malformed.\n" 129 "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"\n" 130 "If value contains comma ',' then ensure the value field is properly quoted " 131 "and escaped" 132 ) 133 endif() 134 string(REGEX MATCH "^[^:]*" map_key "${pair}") 135 string(REGEX REPLACE "^${map_key}:[ ]*" "" value "${pair}") 136 string(STRIP "${map_key}" map_key) 137 if(value MATCHES "," AND NOT (value MATCHES "\\\\," AND value MATCHES "'.*'")) 138 message(FATAL_ERROR "value: ${value} is not properly quoted") 139 endif() 140 string(REGEX REPLACE "\\\\," "," value "${value}") 141 list(APPEND quoted_map_value "\"${map_key}\": \"${value}\"") 142 endforeach() 143 list(JOIN quoted_map_value "," quoted_map_value) 144 string(JSON json_content SET "${json_content}" ${key} ${i} "{${quoted_map_value}}") 145 endforeach() 146 else() 147 math(EXPR stop "${index} + ${length} - 1") 148 list(GET ARG_YAML_LIST 0 entry_0) 149 foreach(i RANGE ${index} ${stop}) 150 list(POP_FRONT ARGN value) 151 string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"") 152 endforeach() 153 endif() 154 endif() 155 endif() 156 set(${var} "${json_content}" PARENT_SCOPE) 157endfunction() 158 159# Usage 160# yaml_context(EXISTS NAME <name> <result>) 161# 162# Function to query the status of the YAML context with the name <name>. 163# The result of the query is stored in <result> 164# 165# EXISTS : Check if the YAML context exists in the current scope 166# If the context exists, then TRUE is returned in <result> 167# NAME <name>: Name of the YAML context 168# <result> : Variable to store the result of the query. 169# 170function(yaml_context) 171 cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN}) 172 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME) 173 174 if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS) 175 message(FATAL_ERROR "Missing argument in " 176 "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} <result-var>)." 177 ) 178 endif() 179 180 zephyr_scope_exists(scope_defined ${ARG_YAML_NAME}) 181 if(scope_defined) 182 list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var) 183 set(${out-var} TRUE PARENT_SCOPE) 184 else() 185 set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE) 186 endif() 187endfunction() 188 189# Usage: 190# yaml_create(NAME <name> [FILE <file>]) 191# 192# Create a new empty YAML context. 193# Use the file <file> for storing the context when 'yaml_save(NAME <name>)' is 194# called. 195# 196# Values can be set by calling 'yaml_set(NAME <name>)' by using the <name> 197# specified when creating the YAML context. 198# 199# NAME <name>: Name of the YAML context. 200# FILE <file>: Path to file to be used together with this YAML context. 201# 202function(yaml_create) 203 cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) 204 205 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 206 207 internal_yaml_context_free(NAME ${ARG_YAML_NAME}) 208 zephyr_create_scope(${ARG_YAML_NAME}) 209 if(DEFINED ARG_YAML_FILE) 210 zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) 211 endif() 212 zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) 213 zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME}) 214endfunction() 215 216# Usage: 217# yaml_load(FILE <file> NAME <name>) 218# 219# Load an existing YAML file and store its content in the YAML context <name>. 220# 221# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using 222# the same YAML scope name. 223# 224# FILE <file>: Path to file to load. 225# NAME <name>: Name of the YAML context. 226# 227function(yaml_load) 228 cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) 229 230 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME) 231 internal_yaml_context_free(NAME ${ARG_YAML_NAME}) 232 233 zephyr_create_scope(${ARG_YAML_NAME}) 234 zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) 235 236 execute_process(COMMAND ${PYTHON_EXECUTABLE} -c 237 "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}')) or {}))" 238 OUTPUT_VARIABLE json_load_out 239 ERROR_VARIABLE json_load_error 240 RESULT_VARIABLE json_load_result 241 ) 242 243 if(json_load_result) 244 message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n" 245 "${json_load_error}" 246 ) 247 endif() 248 249 zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME}) 250 zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME}) 251endfunction() 252 253# Usage: 254# yaml_get(<out-var> NAME <name> KEY <key>...) 255# 256# Get the value of the given key and store the value in <out-var>. 257# If key represents a list, then the list is returned. 258# 259# Behavior is undefined if key points to a complex object. 260# 261# NAME <name> : Name of the YAML context. 262# KEY <key>... : Name of key. 263# <out-var> : Name of output variable. 264# 265function(yaml_get out_var) 266 # Current limitation: 267 # - Anything will be returned, even json object strings. 268 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 269 270 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 271 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 272 273 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 274 275 # We specify error variable to avoid a fatal error. 276 # If key is not found, then type becomes '-NOTFOUND' and value handling is done below. 277 string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) 278 if(type STREQUAL ARRAY) 279 string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) 280 string(JSON arraylength LENGTH "${subjson}") 281 set(array) 282 math(EXPR arraystop "${arraylength} - 1") 283 if(arraylength GREATER 0) 284 foreach(i RANGE 0 ${arraystop}) 285 string(JSON item GET "${subjson}" ${i}) 286 list(APPEND array ${item}) 287 endforeach() 288 endif() 289 set(${out_var} ${array} PARENT_SCOPE) 290 else() 291 # We specify error variable to avoid a fatal error. 292 # Searching for a non-existing key should just result in the output value '-NOTFOUND' 293 string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY}) 294 set(${out_var} ${value} PARENT_SCOPE) 295 endif() 296endfunction() 297 298# Usage: 299# yaml_length(<out-var> NAME <name> KEY <key>...) 300# 301# Get the length of the array defined by the given key and store the length in <out-var>. 302# If key does not define an array, then the length -1 is returned. 303# 304# NAME <name> : Name of the YAML context. 305# KEY <key>... : Name of key defining the list. 306# <out-var> : Name of output variable. 307# 308function(yaml_length out_var) 309 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 310 311 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 312 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 313 314 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 315 316 string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) 317 if(type STREQUAL ARRAY) 318 string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) 319 string(JSON arraylength LENGTH "${subjson}") 320 set(${out_var} ${arraylength} PARENT_SCOPE) 321 elseif(type MATCHES ".*-NOTFOUND") 322 set(${out_var} ${type} PARENT_SCOPE) 323 else() 324 message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.") 325 set(${out_var} -1 PARENT_SCOPE) 326 endif() 327endfunction() 328 329# Usage: 330# yaml_set(NAME <name> KEY <key>... [GENEX] VALUE <value>) 331# yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...) 332# yaml_set(NAME <name> KEY <key>... [APPEND] LIST MAP <map1> MAP <map2> MAP ...) 333# 334# Set a value or a list of values to given key. 335# 336# If setting a list of values, then APPEND can be specified to indicate that the 337# list of values should be appended to the existing list identified with key(s). 338# 339# NAME <name> : Name of the YAML context. 340# KEY <key>... : Name of key. 341# VALUE <value>: New value for the key. 342# LIST <values>: New list of values for the key. 343# APPEND : Append the list of values to the list of values for the key. 344# GENEX : The value(s) contain generator expressions. When using this 345# option, also see the notes in the yaml_save() function. 346# MAP <map> : Map, with key-value pairs where key-value is separated by ':', 347# and pairs separated by ','. 348# Format example: "<key1>: <value1>, <key2>: <value2>, ..." 349# MAP can be given multiple times to separate maps when adding them to a list. 350# LIST MAP cannot be used with GENEX. 351# 352# Note: if a map value contains commas, ',', then the value string must be quoted in 353# single quotes and commas must be double escaped, like this: 'A \\,string' 354# 355function(yaml_set) 356 cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN}) 357 358 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 359 zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) 360 zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) 361 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 362 363 if(ARG_YAML_GENEX) 364 zephyr_set(GENEX TRUE SCOPE ${ARG_YAML_NAME}) 365 endif() 366 367 if(DEFINED ARG_YAML_LIST 368 OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES) 369 set(key_is_list TRUE) 370 endif() 371 372 if(ARG_YAML_APPEND AND NOT key_is_list) 373 message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}(APPEND ...) can only be used with argument: LIST") 374 endif() 375 376 if(ARG_YAML_GENEX AND MAP IN_LIST ARG_YAML_LIST) 377 message(FATAL_ERROR "${function}(GENEX ...) cannot be used with argument: LIST MAP") 378 endif() 379 380 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 381 zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) 382 383 set(yaml_key_undefined ${ARG_YAML_KEY}) 384 foreach(k ${yaml_key_undefined}) 385 list(REMOVE_AT yaml_key_undefined 0) 386 # We ignore any errors as we are checking for existence of the key, and 387 # non-existing keys will throw errors but also set type to NOT-FOUND. 388 string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k}) 389 390 if(NOT type) 391 list(APPEND yaml_key_create ${k}) 392 break() 393 endif() 394 list(APPEND valid_keys ${k}) 395 endforeach() 396 397 list(REVERSE yaml_key_undefined) 398 if(NOT "${yaml_key_undefined}" STREQUAL "") 399 if(key_is_list) 400 internal_yaml_list_initializer(json_string ${genex}) 401 else() 402 set(json_string "\"\"") 403 endif() 404 405 foreach(k ${yaml_key_undefined}) 406 set(json_string "{\"${k}\": ${json_string}}") 407 endforeach() 408 string(JSON json_content SET "${json_content}" 409 ${valid_keys} ${yaml_key_create} "${json_string}" 410 ) 411 endif() 412 413 if(key_is_list) 414 if(NOT ARG_YAML_APPEND) 415 internal_yaml_list_initializer(json_string ${genex}) 416 string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}") 417 endif() 418 zephyr_string(ESCAPE escape_list "${ARG_YAML_LIST}") 419 internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${escape_list}) 420 else() 421 zephyr_string(ESCAPE escape_value "${ARG_YAML_VALUE}") 422 string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${escape_value}\"") 423 endif() 424 425 zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) 426endfunction() 427 428# Usage: 429# yaml_remove(NAME <name> KEY <key>...) 430# 431# Remove the KEY <key>... from the YAML context <name>. 432# 433# Several levels of keys can be given, for example: 434# KEY build cmake command 435# 436# To remove the key 'command' underneath 'cmake' in the toplevel 'build' 437# 438# NAME <name>: Name of the YAML context. 439# KEY <key> : Name of key to remove. 440# 441function(yaml_remove) 442 cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) 443 444 zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) 445 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 446 447 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 448 string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY}) 449 450 zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) 451endfunction() 452 453# Usage: 454# yaml_save(NAME <name> [FILE <file>]) 455# 456# Write the YAML context <name> to <file>, or the one given with the earlier 457# 'yaml_load()' or 'yaml_create()' call. This will be performed immediately if 458# the context does not use generator expressions; otherwise, keys that include 459# a generator expression will initially be written as comments, and the full 460# contents will be available at build time. Build steps that depend on the file 461# being complete must depend on the '<name>_yaml_saved' target. 462# 463# NAME <name>: Name of the YAML context 464# FILE <file>: Path to file to write the context. 465# If not given, then the FILE property of the YAML context will be 466# used. In case both FILE is omitted and FILE property is missing 467# on the YAML context, then an error will be raised. 468# 469function(yaml_save) 470 cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN}) 471 472 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) 473 internal_yaml_context_required(NAME ${ARG_YAML_NAME}) 474 475 zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) 476 if(NOT yaml_file) 477 zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE) 478 endif() 479 if(DEFINED ARG_YAML_FILE) 480 set(yaml_file ${ARG_YAML_FILE}) 481 else() 482 zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE) 483 endif() 484 485 zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) 486 zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) 487 if(genex) 488 to_yaml("${json_content}" 0 yaml_out DIRECT_GENEX) 489 else() 490 to_yaml("${json_content}" 0 yaml_out DIRECT) 491 endif() 492 493 if(EXISTS ${yaml_file}) 494 FILE(RENAME ${yaml_file} ${yaml_file}.bak) 495 endif() 496 FILE(WRITE ${yaml_file} "${yaml_out}") 497 498 set(save_target ${ARG_YAML_NAME}_yaml_saved) 499 if(NOT TARGET ${save_target}) 500 # Create a target for the completion of the YAML save operation. 501 # This will be a dummy unless genexes are used. 502 add_custom_target(${save_target} ALL DEPENDS ${yaml_file}) 503 set_target_properties(${save_target} PROPERTIES 504 genex_save_count 0 505 temp_files "" 506 ) 507 endif() 508 509 if(genex) 510 get_property(genex_save_count TARGET ${save_target} PROPERTY genex_save_count) 511 if(${genex_save_count} EQUAL 0) 512 # First yaml_save() for this context with genexes enabled 513 add_custom_command( 514 OUTPUT ${yaml_file} 515 DEPENDS $<TARGET_PROPERTY:${save_target},expanded_file> 516 COMMAND ${CMAKE_COMMAND} 517 -DEXPANDED_FILE="$<TARGET_PROPERTY:${save_target},expanded_file>" 518 -DOUTPUT_FILE="${yaml_file}" 519 -DTEMP_FILES="$<TARGET_PROPERTY:${save_target},temp_files>" 520 -P ${ZEPHYR_BASE}/cmake/yaml-filter.cmake 521 ) 522 endif() 523 524 math(EXPR genex_save_count "${genex_save_count} + 1") 525 set_property(TARGET ${save_target} PROPERTY genex_save_count ${genex_save_count}) 526 527 cmake_path(SET yaml_path "${yaml_file}") 528 cmake_path(GET yaml_path STEM yaml_file_no_ext) 529 set(expanded_file ${CMAKE_CURRENT_BINARY_DIR}/${yaml_file_no_ext}_${genex_save_count}.yaml) 530 set_property(TARGET ${save_target} PROPERTY expanded_file ${expanded_file}) 531 532 # comment this to keep the temporary files 533 set_property(TARGET ${save_target} APPEND PROPERTY temp_files ${expanded_file}) 534 535 to_yaml("${json_content}" 0 yaml_out TEMP_GENEX) 536 FILE(GENERATE OUTPUT ${expanded_file} CONTENT "${yaml_out}") 537 FILE(TOUCH ${expanded_file}) # ensure timestamp is updated even if nothing changed 538 endif() 539endfunction() 540 541function(to_yaml json level yaml mode) 542 if(mode STREQUAL "DIRECT") 543 # Direct output mode, no genexes: write a standard YAML 544 set(expand_lists TRUE) 545 set(escape_quotes TRUE) 546 set(comment_genexes FALSE) 547 elseif(mode STREQUAL "DIRECT_GENEX" OR mode STREQUAL "FINAL_GENEX") 548 # Direct output mode with genexes enabled, or final write of post-processed 549 # file: write a standard YAML, comment entries with genexes if they are 550 # (still) present in the file 551 set(expand_lists TRUE) 552 set(escape_quotes TRUE) 553 set(comment_genexes TRUE) 554 elseif(mode STREQUAL "TEMP_GENEX") 555 # Temporary output mode for genex expansion: save single quotes with no 556 # special processing, since they will be fixed up by yaml-filter.cmake 557 set(expand_lists FALSE) 558 set(escape_quotes FALSE) 559 set(comment_genexes FALSE) 560 else() 561 message(FATAL_ERROR "to_yaml(... ${mode} ) is malformed.") 562 endif() 563 564 if(level EQUAL 0) 565 # Top-level call, initialize the YAML output variable 566 set(${yaml} "" PARENT_SCOPE) 567 else() 568 math(EXPR level_dec "${level} - 1") 569 set(indent_${level} "${indent_${level_dec}} ") 570 endif() 571 572 string(JSON length LENGTH "${json}") 573 if(length EQUAL 0) 574 # Empty object 575 return() 576 endif() 577 578 math(EXPR stop "${length} - 1") 579 foreach(i RANGE 0 ${stop}) 580 string(JSON member MEMBER "${json}" ${i}) 581 582 string(JSON type TYPE "${json}" ${member}) 583 string(JSON subjson GET "${json}" ${member}) 584 if(type STREQUAL OBJECT) 585 # JSON object -> YAML dictionary 586 set(${yaml} "${${yaml}}${indent_${level}}${member}:\n") 587 math(EXPR sublevel "${level} + 1") 588 to_yaml("${subjson}" ${sublevel} ${yaml} ${mode}) 589 elseif(type STREQUAL ARRAY) 590 # JSON array -> YAML list 591 set(${yaml} "${${yaml}}${indent_${level}}${member}:") 592 string(JSON arraylength LENGTH "${subjson}") 593 if(${arraylength} LESS 1) 594 set(${yaml} "${${yaml}} []\n") 595 else() 596 set(${yaml} "${${yaml}}\n") 597 math(EXPR arraystop "${arraylength} - 1") 598 foreach(i RANGE 0 ${arraystop}) 599 string(JSON item GET "${json}" ${member} ${i}) 600 # Check the length of item. Only OBJECT and ARRAY may have length, so a length at this 601 # level means `to_yaml()` should be called recursively. 602 string(JSON length ERROR_VARIABLE ignore LENGTH "${item}") 603 if(length) 604 set(non_indent_yaml) 605 to_yaml("${item}" 0 non_indent_yaml ${mode}) 606 string(REGEX REPLACE "\n$" "" non_indent_yaml "${non_indent_yaml}") 607 string(REPLACE "\n" "\n${indent_${level}} " indent_yaml "${non_indent_yaml}") 608 set(${yaml} "${${yaml}}${indent_${level}} - ${indent_yaml}\n") 609 else() 610 # Assume a string, escape single quotes when required (see comment below). 611 if(escape_quotes) 612 string(REPLACE "'" "''" item "${item}") 613 endif() 614 set(${yaml} "${${yaml}}${indent_${level}} - '${item}'\n") 615 endif() 616 endforeach() 617 endif() 618 elseif(type STREQUAL STRING) 619 # JSON string maps to multiple YAML types: 620 # - with unexpanded generator expressions: save as YAML comment 621 # - if it matches the special prefix: convert to YAML list 622 # - otherwise: save as YAML scalar 623 # Single quotes must be escaped in the value _unless_ this will be used 624 # to expand generator expressions, because then the escaping will be 625 # addressed once in the yaml-filter.cmake script. 626 if(escape_quotes) 627 string(REPLACE "'" "''" subjson "${subjson}") 628 endif() 629 if(subjson MATCHES "\\$<.*>" AND comment_genexes) 630 # Yet unexpanded generator expression: save as comment 631 string(SUBSTRING ${indent_${level}} 1 -1 short_indent) 632 set(${yaml} "${${yaml}}#${short_indent}${member}: '${subjson}'\n") 633 elseif(subjson MATCHES "^@YAML-LIST@" AND expand_lists) 634 # List-as-string: convert to list 635 set(${yaml} "${${yaml}}${indent_${level}}${member}:") 636 list(POP_FRONT subjson) 637 if(subjson STREQUAL "") 638 set(${yaml} "${${yaml}} []\n") 639 else() 640 set(${yaml} "${${yaml}}\n") 641 foreach(item ${subjson}) 642 set(${yaml} "${${yaml}}${indent_${level}} - '${item}'\n") 643 endforeach() 644 endif() 645 else() 646 # Raw strings: save as is 647 set(${yaml} "${${yaml}}${indent_${level}}${member}: '${subjson}'\n") 648 endif() 649 else() 650 # Other JSON data type -> YAML scalar, as-is 651 set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n") 652 endif() 653 endforeach() 654 655 set(${yaml} ${${yaml}} PARENT_SCOPE) 656endfunction() 657