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