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