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# All of above can be combined, for example like:
23# foo:
24#   bar: baz
25#   quz:
26#     greek:
27#      - alpha
28#      - beta
29#      - gamma
30# fred: thud
31#
32# Support for list of objects are currently experimental and not guranteed to work.
33# For example:
34# foo:
35#  - bar: val1
36#    baz: val1
37#  - bar: val2
38#    baz: val2
39
40include_guard(GLOBAL)
41
42include(extensions)
43include(python)
44
45# Internal helper function for checking that a YAML context has been created
46# before operating on it.
47# Will result in CMake error if context does not exist.
48function(internal_yaml_context_required)
49  cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN})
50  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
51  yaml_context(EXISTS NAME ${ARG_YAML_NAME} result)
52
53  if(NOT result)
54    message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' does not exist."
55            "Remember to create a YAML context using 'yaml_create()' or 'yaml_load()'"
56    )
57  endif()
58endfunction()
59
60# Internal helper function for checking if a YAML context is free before creating
61# it later.
62# Will result in CMake error if context exists.
63function(internal_yaml_context_free)
64  cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN})
65  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
66  yaml_context(EXISTS NAME ${ARG_YAML_NAME} result)
67
68  if(result)
69    message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' already exists."
70            "Please create a YAML context with a unique name"
71    )
72  endif()
73endfunction()
74
75# Usage
76#   yaml_context(EXISTS NAME <name> <result>)
77#
78# Function to query the status of the YAML context with the name <name>.
79# The result of the query is stored in <result>
80#
81# EXISTS     : Check if the YAML context exists in the current scope
82#              If the context exists, then TRUE is returned in <result>
83# NAME <name>: Name of the YAML context
84# <result>   : Variable to store the result of the query.
85#
86function(yaml_context)
87  cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN})
88  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME)
89
90  if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS)
91    message(FATAL_ERROR "Missing argument in "
92            "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} <result-var>)."
93    )
94  endif()
95
96  zephyr_scope_exists(scope_defined ${ARG_YAML_NAME})
97  if(scope_defined)
98    list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var)
99    set(${out-var} TRUE PARENT_SCOPE)
100  else()
101    set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE)
102  endif()
103endfunction()
104
105# Usage:
106#   yaml_create(NAME <name> [FILE <file>])
107#
108# Create a new empty YAML context.
109# Use the file <file> for storing the context when 'yaml_save(NAME <name>)' is
110# called.
111#
112# Values can be set by calling 'yaml_set(NAME <name>)' by using the <name>
113# specified when creating the YAML context.
114#
115# NAME <name>: Name of the YAML context.
116# FILE <file>: Path to file to be used together with this YAML context.
117#
118function(yaml_create)
119  cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN})
120
121  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
122
123  internal_yaml_context_free(NAME ${ARG_YAML_NAME})
124  zephyr_create_scope(${ARG_YAML_NAME})
125  if(DEFINED ARG_YAML_FILE)
126    zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
127  endif()
128  zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME})
129endfunction()
130
131# Usage:
132#   yaml_load(FILE <file> NAME <name>)
133#
134# Load an existing YAML file and store its content in the YAML context <name>.
135#
136# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using
137# the same YAML scope name.
138#
139# FILE <file>: Path to file to load.
140# NAME <name>: Name of the YAML context.
141#
142function(yaml_load)
143  cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN})
144
145  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME)
146  internal_yaml_context_free(NAME ${ARG_YAML_NAME})
147
148  zephyr_create_scope(${ARG_YAML_NAME})
149  zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
150
151  execute_process(COMMAND ${PYTHON_EXECUTABLE} -c
152    "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}'))))"
153    OUTPUT_VARIABLE json_load_out
154    ERROR_VARIABLE json_load_error
155    RESULT_VARIABLE json_load_result
156  )
157
158  if(json_load_result)
159    message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n"
160                        "${json_load_error}"
161    )
162  endif()
163
164  zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME})
165endfunction()
166
167# Usage:
168#   yaml_get(<out-var> NAME <name> KEY <key>...)
169#
170# Get the value of the given key and store the value in <out-var>.
171# If key represents a list, then the list is returned.
172#
173# Behavior is undefined if key points to a complex object.
174#
175# NAME <name>  : Name of the YAML context.
176# KEY <key>... : Name of key.
177# <out-var>    : Name of output variable.
178#
179function(yaml_get out_var)
180  # Current limitation:
181  # - Anything will be returned, even json object strings.
182  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
183
184  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
185  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
186
187  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
188
189  # We specify error variable to avoid a fatal error.
190  # If key is not found, then type becomes '-NOTFOUND' and value handling is done below.
191  string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY})
192  if(type STREQUAL ARRAY)
193    string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY})
194    string(JSON arraylength LENGTH "${subjson}")
195    set(array)
196    math(EXPR arraystop "${arraylength} - 1")
197    if(arraylength GREATER 0)
198      foreach(i RANGE 0 ${arraystop})
199        string(JSON item GET "${subjson}" ${i})
200        list(APPEND array ${item})
201      endforeach()
202    endif()
203    set(${out_var} ${array} PARENT_SCOPE)
204  else()
205    # We specify error variable to avoid a fatal error.
206    # Searching for a non-existing key should just result in the output value '-NOTFOUND'
207    string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY})
208    set(${out_var} ${value} PARENT_SCOPE)
209  endif()
210endfunction()
211
212# Usage:
213#   yaml_length(<out-var> NAME <name> KEY <key>...)
214#
215# Get the length of the array defined by the given key and store the length in <out-var>.
216# If key does not define an array, then the length -1 is returned.
217#
218# NAME <name>  : Name of the YAML context.
219# KEY <key>... : Name of key defining the list.
220# <out-var>    : Name of output variable.
221#
222function(yaml_length out_var)
223  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
224
225  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
226  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
227
228  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
229
230  string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY})
231  if(type STREQUAL ARRAY)
232    string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY})
233    string(JSON arraylength LENGTH "${subjson}")
234    set(${out_var} ${arraylength} PARENT_SCOPE)
235  elseif(type MATCHES ".*-NOTFOUND")
236    set(${out_var} ${type} PARENT_SCOPE)
237  else()
238    message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.")
239    set(${out_var} -1 PARENT_SCOPE)
240  endif()
241endfunction()
242
243# Usage:
244#   yaml_set(NAME <name> KEY <key>... VALUE <value>)
245#   yaml_set(NAME <name> KEY <key>... [APPEND] LIST <value>...)
246#
247# Set a value or a list of values to given key.
248#
249# If setting a list of values, then APPEND can be specified to indicate that the
250# list of values should be appended to the existing list identified with key(s).
251#
252# NAME <name>  : Name of the YAML context.
253# KEY <key>... : Name of key.
254# VALUE <value>: New value for the key.
255# List <values>: New list of values for the key.
256# APPEND       : Append the list of values to the list of values for the key.
257#
258function(yaml_set)
259  cmake_parse_arguments(ARG_YAML "APPEND" "NAME;VALUE" "KEY;LIST" ${ARGN})
260
261  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
262  zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
263  zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
264  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
265
266  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
267
268  set(yaml_key_undefined ${ARG_YAML_KEY})
269  foreach(k ${yaml_key_undefined})
270    list(REMOVE_AT yaml_key_undefined 0)
271    # We ignore any errors as we are checking for existence of the key, and
272    # non-existing keys will throw errors but also set type to NOT-FOUND.
273    string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k})
274
275    if(NOT type)
276      list(APPEND yaml_key_create ${k})
277      break()
278    endif()
279    list(APPEND valid_keys ${k})
280  endforeach()
281
282  list(REVERSE yaml_key_undefined)
283  if(NOT "${yaml_key_undefined}" STREQUAL "")
284    if(ARG_YAML_APPEND)
285      set(json_string "[]")
286    else()
287      set(json_string "\"\"")
288    endif()
289
290    foreach(k ${yaml_key_undefined})
291      set(json_string "{\"${k}\": ${json_string}}")
292    endforeach()
293    string(JSON json_content SET "${json_content}"
294           ${valid_keys} ${yaml_key_create} "${json_string}"
295    )
296  endif()
297
298  if(DEFINED ARG_YAML_LIST OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES)
299    if(NOT ARG_YAML_APPEND)
300      string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "[]")
301    endif()
302
303    string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY})
304    string(JSON index LENGTH "${subjson}")
305    list(LENGTH ARG_YAML_LIST length)
306    math(EXPR stop "${index} + ${length} - 1")
307    if(NOT length EQUAL 0)
308      foreach(i RANGE ${index} ${stop})
309        list(POP_FRONT ARG_YAML_LIST value)
310        string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} ${i} "\"${value}\"")
311      endforeach()
312    endif()
313  else()
314    string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${ARG_YAML_VALUE}\"")
315  endif()
316
317  zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME})
318endfunction()
319
320# Usage:
321#   yaml_remove(NAME <name> KEY <key>...)
322#
323# Remove the KEY <key>... from the YAML context <name>.
324#
325# Several levels of keys can be given, for example:
326# KEY build cmake command
327#
328# To remove the key 'command' underneath 'cmake' in the toplevel 'build'
329#
330# NAME <name>: Name of the YAML context.
331# KEY <key>  : Name of key to remove.
332#
333function(yaml_remove)
334  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
335
336  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
337  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
338
339  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
340  string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY})
341
342  zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME})
343endfunction()
344
345# Usage:
346#   yaml_save(NAME <name> [FILE <file>])
347#
348# Write the YAML context <name> to the file which were given with the earlier
349# 'yaml_load()' or 'yaml_create()' call.
350#
351# NAME <name>: Name of the YAML context
352# FILE <file>: Path to file to write the context.
353#              If not given, then the FILE property of the YAML context will be
354#              used. In case both FILE is omitted and FILE property is missing
355#              on the YAML context, then an error will be raised.
356#
357function(yaml_save)
358  cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN})
359
360  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
361  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
362
363  zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE)
364  if(NOT yaml_file)
365    zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE)
366  endif()
367
368  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
369  to_yaml("${json_content}" 0 yaml_out)
370
371  if(DEFINED ARG_YAML_FILE)
372    set(yaml_file ${ARG_YAML_FILE})
373  else()
374    zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE)
375  endif()
376  if(EXISTS ${yaml_file})
377    FILE(RENAME ${yaml_file} ${yaml_file}.bak)
378  endif()
379  FILE(WRITE ${yaml_file} "${yaml_out}")
380endfunction()
381
382function(to_yaml json level yaml)
383  if(level GREATER 0)
384    math(EXPR level_dec "${level} - 1")
385    set(indent_${level} "${indent_${level_dec}}  ")
386  endif()
387
388  string(JSON length LENGTH "${json}")
389  if(length EQUAL 0)
390    # Empty object
391    return()
392  endif()
393
394  math(EXPR stop "${length} - 1")
395  foreach(i RANGE 0 ${stop})
396    string(JSON member MEMBER "${json}" ${i})
397
398    string(JSON type TYPE "${json}" ${member})
399    string(JSON subjson GET "${json}" ${member})
400    if(type STREQUAL OBJECT)
401      set(${yaml} "${${yaml}}${indent_${level}}${member}:\n")
402      math(EXPR sublevel "${level} + 1")
403      to_yaml("${subjson}" ${sublevel} ${yaml})
404    elseif(type STREQUAL ARRAY)
405      set(${yaml} "${${yaml}}${indent_${level}}${member}:")
406      string(JSON arraylength LENGTH "${subjson}")
407      if(${arraylength} LESS 1)
408        set(${yaml} "${${yaml}} []\n")
409      else()
410        set(${yaml} "${${yaml}}\n")
411        math(EXPR arraystop "${arraylength} - 1")
412        foreach(i RANGE 0 ${arraystop})
413          string(JSON item GET "${json}" ${member} ${i})
414          set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n")
415        endforeach()
416      endif()
417    else()
418      set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n")
419    endif()
420  endforeach()
421
422  set(${yaml} ${${yaml}} PARENT_SCOPE)
423endfunction()
424