diff --git a/cmake/modules/yaml.cmake b/cmake/modules/yaml.cmake new file mode 100644 index 00000000000..50d6c7cc746 --- /dev/null +++ b/cmake/modules/yaml.cmake @@ -0,0 +1,422 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2024, Nordic Semiconductor ASA + +# CMake YAML module for handling of YAML files. +# +# This module offers basic support for simple yaml files. +# +# It supports basic key-value pairs, like +# foo: bar +# +# basic key-object pairs, like +# foo: +# bar: baz +# +# Simple value lists, like: +# foos: +# - foo1 +# - foo2 +# - foo3 +# +# All of above can be combined, for example like: +# foo: +# bar: baz +# quz: +# greek: +# - alpha +# - beta +# - gamma +# fred: thud +# +# Support for list of objects are currently experimental and not guranteed to work. +# For example: +# foo: +# - bar: val1 +# baz: val1 +# - bar: val2 +# baz: val2 + +include_guard(GLOBAL) + +include(extensions) +include(python) + +# Internal helper function for checking that a YAML context has been created +# before operating on it. +# Will result in CMake error if context does not exist. +function(internal_yaml_context_required) + cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) + zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) + yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) + + if(NOT result) + message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' does not exist." + "Remember to create a YAML context using 'yaml_create()' or 'yaml_load()'" + ) + endif() +endfunction() + +# Internal helper function for checking if a YAML context is free before creating +# it later. +# Will result in CMake error if context exists. +function(internal_yaml_context_free) + cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN}) + zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) + yaml_context(EXISTS NAME ${ARG_YAML_NAME} result) + + if(result) + message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' already exists." + "Please create a YAML context with a unique name" + ) + endif() +endfunction() + +# Usage +# yaml_context(EXISTS NAME ) +# +# Function to query the status of the YAML context with the name . +# The result of the query is stored in +# +# EXISTS : Check if the YAML context exists in the current scope +# If the context exists, then TRUE is returned in +# NAME : Name of the YAML context +# : Variable to store the result of the query. +# +function(yaml_context) + cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN}) + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME) + + if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Missing argument in " + "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} )." + ) + endif() + + if(TARGET ${ARG_YAML_NAME}_scope) + list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var) + set(${out-var} TRUE PARENT_SCOPE) + else() + set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE) + endif() +endfunction() + +# Usage: +# yaml_create(NAME [FILE ]) +# +# Create a new empty YAML context. +# Use the file for storing the context when 'yaml_save(NAME )' is +# called. +# +# Values can be set by calling 'yaml_set(NAME )' by using the +# specified when creating the YAML context. +# +# NAME : Name of the YAML context. +# FILE : Path to file to be used together with this YAML context. +# +function(yaml_create) + cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) + + zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) + + internal_yaml_context_free(NAME ${ARG_YAML_NAME}) + zephyr_create_scope(${ARG_YAML_NAME}) + if(DEFINED ARG_YAML_FILE) + zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) + endif() + zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME}) +endfunction() + +# Usage: +# yaml_load(FILE NAME ) +# +# Load an existing YAML file and store its content in the YAML context . +# +# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using +# the same YAML scope name. +# +# FILE : Path to file to load. +# NAME : Name of the YAML context. +# +function(yaml_load) + cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN}) + + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME) + internal_yaml_context_free(NAME ${ARG_YAML_NAME}) + + zephyr_create_scope(${ARG_YAML_NAME}) + zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME}) + + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c + "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}'))))" + OUTPUT_VARIABLE json_load_out + ERROR_VARIABLE json_load_error + RESULT_VARIABLE json_load_result + ) + + if(json_load_result) + message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n" + "${json_load_error}" + ) + endif() + + zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME}) +endfunction() + +# Usage: +# yaml_get( NAME KEY ...) +# +# Get the value of the given key and store the value in . +# If key represents a list, then the list is returned. +# +# Behavior is undefined if key points to a complex object. +# +# NAME : Name of the YAML context. +# KEY ... : Name of key. +# : Name of output variable. +# +function(yaml_get out_var) + # Current limitation: + # - Anything will be returned, even json object strings. + cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) + + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) + internal_yaml_context_required(NAME ${ARG_YAML_NAME}) + + get_property(json_content TARGET ${ARG_YAML_NAME}_scope PROPERTY JSON) + + # We specify error variable to avoid a fatal error. + # If key is not found, then type becomes '-NOTFOUND' and value handling is done below. + string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) + if(type STREQUAL ARRAY) + string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) + string(JSON arraylength LENGTH "${subjson}") + set(array) + math(EXPR arraystop "${arraylength} - 1") + if(arraylength GREATER 0) + foreach(i RANGE 0 ${arraystop}) + string(JSON item GET "${subjson}" ${i}) + list(APPEND array ${item}) + endforeach() + endif() + set(${out_var} ${array} PARENT_SCOPE) + else() + # We specify error variable to avoid a fatal error. + # Searching for a non-existing key should just result in the output value '-NOTFOUND' + string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY}) + set(${out_var} ${value} PARENT_SCOPE) + endif() +endfunction() + +# Usage: +# yaml_length( NAME KEY ...) +# +# Get the length of the array defined by the given key and store the length in . +# If key does not define an array, then the length -1 is returned. +# +# NAME : Name of the YAML context. +# KEY ... : Name of key defining the list. +# : Name of output variable. +# +function(yaml_length out_var) + cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) + + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) + internal_yaml_context_required(NAME ${ARG_YAML_NAME}) + + get_property(json_content TARGET ${ARG_YAML_NAME}_scope PROPERTY JSON) + + string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY}) + if(type STREQUAL ARRAY) + string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) + string(JSON arraylength LENGTH "${subjson}") + set(${out_var} ${arraylength} PARENT_SCOPE) + elseif(type MATCHES ".*-NOTFOUND") + set(${out_var} ${type} PARENT_SCOPE) + else() + message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.") + set(${out_var} -1 PARENT_SCOPE) + endif() +endfunction() + +# Usage: +# yaml_set(NAME KEY ... VALUE ) +# yaml_set(NAME KEY ... [APPEND] LIST ...) +# +# Set a value or a list of values to given key. +# +# If setting a list of values, then APPEND can be specified to indicate that the +# list of values should be appended to the existing list identified with key(s). +# +# NAME : Name of the YAML context. +# KEY ... : Name of key. +# VALUE : New value for the key. +# List : New list of values for the key. +# APPEND : Append the list of values to the list of values for the key. +# +function(yaml_set) + cmake_parse_arguments(ARG_YAML "APPEND" "NAME;VALUE" "KEY;LIST" ${ARGN}) + + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) + zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) + zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST) + internal_yaml_context_required(NAME ${ARG_YAML_NAME}) + + get_property(json_content TARGET ${ARG_YAML_NAME}_scope PROPERTY JSON) + + set(yaml_key_undefined ${ARG_YAML_KEY}) + foreach(k ${yaml_key_undefined}) + list(REMOVE_AT yaml_key_undefined 0) + # We ignore any errors as we are checking for existence of the key, and + # non-existing keys will throw errors but also set type to NOT-FOUND. + string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k}) + + if(NOT type) + list(APPEND yaml_key_create ${k}) + break() + endif() + list(APPEND valid_keys ${k}) + endforeach() + + list(REVERSE yaml_key_undefined) + if(NOT "${yaml_key_undefined}" STREQUAL "") + if(ARG_YAML_APPEND) + set(json_string "[]") + else() + set(json_string "\"\"") + endif() + + foreach(k ${yaml_key_undefined}) + set(json_string "{\"${k}\": ${json_string}}") + endforeach() + string(JSON json_content SET "${json_content}" + ${valid_keys} ${yaml_key_create} "${json_string}" + ) + endif() + + if(DEFINED ARG_YAML_LIST OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES) + if(NOT ARG_YAML_APPEND) + string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "[]") + endif() + + string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY}) + string(JSON index LENGTH "${subjson}") + list(LENGTH ARG_YAML_LIST length) + math(EXPR stop "${index} + ${length} - 1") + if(NOT length EQUAL 0) + foreach(i RANGE ${index} ${stop}) + list(POP_FRONT ARG_YAML_LIST value) + string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} ${i} "\"${value}\"") + endforeach() + endif() + else() + string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${ARG_YAML_VALUE}\"") + endif() + + zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) +endfunction() + +# Usage: +# yaml_remove(NAME KEY ...) +# +# Remove the KEY ... from the YAML context . +# +# Several levels of keys can be given, for example: +# KEY build cmake command +# +# To remove the key 'command' underneath 'cmake' in the toplevel 'build' +# +# NAME : Name of the YAML context. +# KEY : Name of key to remove. +# +function(yaml_remove) + cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN}) + + zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY) + internal_yaml_context_required(NAME ${ARG_YAML_NAME}) + + get_property(json_content TARGET ${ARG_YAML_NAME}_scope PROPERTY JSON) + string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY}) + + zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME}) +endfunction() + +# Usage: +# yaml_save(NAME [FILE ]) +# +# Write the YAML context to the file which were given with the earlier +# 'yaml_load()' or 'yaml_create()' call. +# +# NAME : Name of the YAML context +# FILE : Path to file to write the context. +# If not given, then the FILE property of the YAML context will be +# used. In case both FILE is omitted and FILE property is missing +# on the YAML context, then an error will be raised. +# +function(yaml_save) + cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN}) + + zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME) + internal_yaml_context_required(NAME ${ARG_YAML_NAME}) + + get_target_property(yaml_file ${ARG_YAML_NAME}_scope FILE) + if(NOT yaml_file) + zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE) + endif() + + get_property(json_content TARGET ${ARG_YAML_NAME}_scope PROPERTY JSON) + to_yaml("${json_content}" 0 yaml_out) + + if(DEFINED ARG_YAML_FILE) + set(yaml_file ${ARG_YAML_FILE}) + else() + get_property(yaml_file TARGET ${ARG_YAML_NAME}_scope PROPERTY FILE) + endif() + if(EXISTS ${yaml_file}) + FILE(RENAME ${yaml_file} ${yaml_file}.bak) + endif() + FILE(WRITE ${yaml_file} "${yaml_out}") +endfunction() + +function(to_yaml json level yaml) + if(level GREATER 0) + math(EXPR level_dec "${level} - 1") + set(indent_${level} "${indent_${level_dec}} ") + endif() + + string(JSON length LENGTH "${json}") + if(length EQUAL 0) + # Empty object + return() + endif() + + math(EXPR stop "${length} - 1") + foreach(i RANGE 0 ${stop}) + string(JSON member MEMBER "${json}" ${i}) + + string(JSON type TYPE "${json}" ${member}) + string(JSON subjson GET "${json}" ${member}) + if(type STREQUAL OBJECT) + set(${yaml} "${${yaml}}${indent_${level}}${member}:\n") + math(EXPR sublevel "${level} + 1") + to_yaml("${subjson}" ${sublevel} ${yaml}) + elseif(type STREQUAL ARRAY) + set(${yaml} "${${yaml}}${indent_${level}}${member}:") + string(JSON arraylength LENGTH "${subjson}") + if(${arraylength} LESS 1) + set(${yaml} "${${yaml}} []\n") + else() + set(${yaml} "${${yaml}}\n") + math(EXPR arraystop "${arraylength} - 1") + foreach(i RANGE 0 ${arraystop}) + string(JSON item GET "${json}" ${member} ${i}) + set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n") + endforeach() + endif() + else() + set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n") + endif() + endforeach() + + set(${yaml} ${${yaml}} PARENT_SCOPE) +endfunction() diff --git a/cmake/modules/zephyr_default.cmake b/cmake/modules/zephyr_default.cmake index 7472331255b..2afef934057 100644 --- a/cmake/modules/zephyr_default.cmake +++ b/cmake/modules/zephyr_default.cmake @@ -82,6 +82,7 @@ list(APPEND zephyr_cmake_modules basic_settings) list(APPEND zephyr_cmake_modules west) list(APPEND zephyr_cmake_modules ccache) +list(APPEND zephyr_cmake_modules yaml) # Load default root settings list(APPEND zephyr_cmake_modules root) diff --git a/share/sysbuild/cmake/modules/sysbuild_default.cmake b/share/sysbuild/cmake/modules/sysbuild_default.cmake index 66ffb7374b3..2d45bc1d934 100644 --- a/share/sysbuild/cmake/modules/sysbuild_default.cmake +++ b/share/sysbuild/cmake/modules/sysbuild_default.cmake @@ -9,6 +9,7 @@ include(extensions) include(sysbuild_extensions) include(python) include(west) +include(yaml) include(sysbuild_root) include(zephyr_module) include(boards)