Skip to content

Commit 1ee5fdf

Browse files
pillo79tejlmand
authored andcommitted
[nrf fromtree] cmake: yaml: add support for generator expressions
This commit adds support for generator expressions in values and lists to the yaml module. Generator expressions can only be expanded by CMake after all configuration code has been executed and the final values of the project properties are defined. This means that contexts that contain generator expressions are written twice: - immediately, during the 'yaml_save()' call, a comment with the raw unexpanded string is saved instead of the key that uses generator expressions in the YAML file; - after the configuration step, a custom command updates the YAML file contents with the fully expanded values. This two-step process also allows to overcome the issue of lists that are extracted from generator expressions, whose elements would be expanded into a single string if written directly to the YAML file. Instead, the lists are stored in their CMake string format with a special marker, expanded by CMake into a temporary JSON file, and the conversion to a proper list is performed during the build step. If the saved YAML file for context <name> is needed by further build steps in this project, the target '<name>_yaml_saved' must be added as a dependency to ensure the final contents are ready. Note that when generator expressions are used in the context, the GENEX keyword must be provided to yaml_set(). This is necessary to avoid storing the genexes as raw strings in the YAML. Signed-off-by: Luca Burelli <[email protected]> (cherry picked from commit cdc7f05)
1 parent 65aacb4 commit 1ee5fdf

File tree

2 files changed

+171
-28
lines changed

2 files changed

+171
-28
lines changed

cmake/modules/yaml.cmake

Lines changed: 136 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,48 @@ endfunction()
7474

7575
# Internal helper function to provide the correct initializer for a list in the
7676
# JSON content.
77-
function(internal_yaml_list_initializer var)
78-
set(${var} "[]" PARENT_SCOPE)
77+
function(internal_yaml_list_initializer var genex)
78+
if(genex)
79+
set(${var} "\"@YAML-LIST@\"" PARENT_SCOPE)
80+
else()
81+
set(${var} "[]" PARENT_SCOPE)
82+
endif()
7983
endfunction()
8084

8185
# Internal helper function to append items to a list in the JSON content.
8286
# Unassigned arguments are the values to be appended.
83-
function(internal_yaml_list_append var key)
87+
function(internal_yaml_list_append var genex key)
8488
set(json_content "${${var}}")
8589
string(JSON subjson GET "${json_content}" ${key})
86-
string(JSON index LENGTH "${subjson}")
87-
list(LENGTH ARGN length)
88-
math(EXPR stop "${index} + ${length} - 1")
89-
if(NOT length EQUAL 0)
90-
foreach(i RANGE ${index} ${stop})
91-
list(POP_FRONT ARGN value)
92-
string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"")
93-
endforeach()
90+
if(genex)
91+
# new lists are stored in CMake string format, but those imported via
92+
# yaml_load() are proper JSON arrays. When an append is requested, those
93+
# must be converted back to a CMake list.
94+
string(JSON type TYPE "${json_content}" ${key})
95+
if(type STREQUAL ARRAY)
96+
string(JSON arraylength LENGTH "${subjson}")
97+
internal_yaml_list_initializer(subjson TRUE)
98+
if(${arraylength} GREATER 0)
99+
math(EXPR arraystop "${arraylength} - 1")
100+
foreach(i RANGE 0 ${arraystop})
101+
string(JSON item GET "${json_content}" ${key} ${i})
102+
list(APPEND subjson ${item})
103+
endforeach()
104+
endif()
105+
endif()
106+
list(APPEND subjson ${ARGN})
107+
string(JSON json_content SET "${json_content}" ${key} "\"${subjson}\"")
108+
else()
109+
# lists are stored as JSON arrays
110+
string(JSON index LENGTH "${subjson}")
111+
list(LENGTH ARGN length)
112+
math(EXPR stop "${index} + ${length} - 1")
113+
if(NOT length EQUAL 0)
114+
foreach(i RANGE ${index} ${stop})
115+
list(POP_FRONT ARGN value)
116+
string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"")
117+
endforeach()
118+
endif()
94119
endif()
95120
set(${var} "${json_content}" PARENT_SCOPE)
96121
endfunction()
@@ -148,6 +173,7 @@ function(yaml_create)
148173
if(DEFINED ARG_YAML_FILE)
149174
zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
150175
endif()
176+
zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME})
151177
zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME})
152178
endfunction()
153179

@@ -172,7 +198,7 @@ function(yaml_load)
172198
zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
173199

174200
execute_process(COMMAND ${PYTHON_EXECUTABLE} -c
175-
"import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}'))))"
201+
"import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}')) or {}))"
176202
OUTPUT_VARIABLE json_load_out
177203
ERROR_VARIABLE json_load_error
178204
RESULT_VARIABLE json_load_result
@@ -184,6 +210,7 @@ function(yaml_load)
184210
)
185211
endif()
186212

213+
zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME})
187214
zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME})
188215
endfunction()
189216

@@ -264,8 +291,8 @@ function(yaml_length out_var)
264291
endfunction()
265292

266293
# Usage:
267-
# yaml_set(NAME <name> KEY <key>... VALUE <value>)
268-
# yaml_set(NAME <name> KEY <key>... [APPEND] LIST <value>...)
294+
# yaml_set(NAME <name> KEY <key>... [GENEX] VALUE <value>)
295+
# yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...)
269296
#
270297
# Set a value or a list of values to given key.
271298
#
@@ -275,18 +302,22 @@ endfunction()
275302
# NAME <name> : Name of the YAML context.
276303
# KEY <key>... : Name of key.
277304
# VALUE <value>: New value for the key.
278-
# List <values>: New list of values for the key.
305+
# LIST <values>: New list of values for the key.
279306
# APPEND : Append the list of values to the list of values for the key.
307+
# GENEX : The value(s) contain generator expressions. When using this
308+
# option, also see the notes in the yaml_save() function.
280309
#
281310
function(yaml_set)
282-
cmake_parse_arguments(ARG_YAML "APPEND" "NAME;VALUE" "KEY;LIST" ${ARGN})
311+
cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN})
283312

284313
zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
285314
zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
286315
zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
287316
internal_yaml_context_required(NAME ${ARG_YAML_NAME})
288317

289-
zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
318+
if(ARG_YAML_GENEX)
319+
zephyr_set(GENEX TRUE SCOPE ${ARG_YAML_NAME})
320+
endif()
290321

291322
if(DEFINED ARG_YAML_LIST
292323
OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES)
@@ -317,7 +348,7 @@ function(yaml_set)
317348
list(REVERSE yaml_key_undefined)
318349
if(NOT "${yaml_key_undefined}" STREQUAL "")
319350
if(key_is_list)
320-
internal_yaml_list_initializer(json_string)
351+
internal_yaml_list_initializer(json_string ${genex})
321352
else()
322353
set(json_string "\"\"")
323354
endif()
@@ -332,11 +363,11 @@ function(yaml_set)
332363

333364
if(key_is_list)
334365
if(NOT ARG_YAML_APPEND)
335-
internal_yaml_list_initializer(json_string)
366+
internal_yaml_list_initializer(json_string ${genex})
336367
string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}")
337368
endif()
338369

339-
internal_yaml_list_append(json_content "${ARG_YAML_KEY}" ${ARG_YAML_LIST})
370+
internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${ARG_YAML_LIST})
340371
else()
341372
string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${ARG_YAML_VALUE}\"")
342373
endif()
@@ -372,8 +403,12 @@ endfunction()
372403
# Usage:
373404
# yaml_save(NAME <name> [FILE <file>])
374405
#
375-
# Write the YAML context <name> to the file which were given with the earlier
376-
# 'yaml_load()' or 'yaml_create()' call.
406+
# Write the YAML context <name> to <file>, or the one given with the earlier
407+
# 'yaml_load()' or 'yaml_create()' call. This will be performed immediately if
408+
# the context does not use generator expressions; otherwise, keys that include
409+
# a generator expression will initially be written as comments, and the full
410+
# contents will be available at build time. Build steps that depend on the file
411+
# being complete must depend on the '<name>_yaml_saved' target.
377412
#
378413
# NAME <name>: Name of the YAML context
379414
# FILE <file>: Path to file to write the context.
@@ -391,22 +426,67 @@ function(yaml_save)
391426
if(NOT yaml_file)
392427
zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE)
393428
endif()
394-
395-
zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
396-
to_yaml("${json_content}" 0 yaml_out)
397-
398429
if(DEFINED ARG_YAML_FILE)
399430
set(yaml_file ${ARG_YAML_FILE})
400431
else()
401432
zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE)
402433
endif()
434+
435+
zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX)
436+
zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
437+
to_yaml("${json_content}" 0 yaml_out ${genex})
438+
403439
if(EXISTS ${yaml_file})
404440
FILE(RENAME ${yaml_file} ${yaml_file}.bak)
405441
endif()
406442
FILE(WRITE ${yaml_file} "${yaml_out}")
443+
444+
set(save_target ${ARG_YAML_NAME}_yaml_saved)
445+
if (NOT TARGET ${save_target})
446+
# Create a target for the completion of the YAML save operation.
447+
# This will be a dummy unless genexes are used.
448+
add_custom_target(${save_target} ALL DEPENDS ${yaml_file})
449+
set_target_properties(${save_target} PROPERTIES
450+
genex_save_count 0
451+
temp_files ""
452+
)
453+
endif()
454+
455+
if (genex)
456+
get_property(genex_save_count TARGET ${save_target} PROPERTY genex_save_count)
457+
if (${genex_save_count} EQUAL 0)
458+
# First yaml_save() for this context with genexes enabled
459+
add_custom_command(
460+
OUTPUT ${yaml_file}
461+
DEPENDS $<TARGET_PROPERTY:${save_target},json_file>
462+
COMMAND ${CMAKE_COMMAND}
463+
-DJSON_FILE="$<TARGET_PROPERTY:${save_target},json_file>"
464+
-DYAML_FILE="${yaml_file}"
465+
-DTEMP_FILES="$<TARGET_PROPERTY:${save_target},temp_files>"
466+
-P ${ZEPHYR_BASE}/cmake/yaml-filter.cmake
467+
)
468+
endif()
469+
470+
math(EXPR genex_save_count "${genex_save_count} + 1")
471+
set_property(TARGET ${save_target} PROPERTY genex_save_count ${genex_save_count})
472+
473+
cmake_path(SET yaml_path "${yaml_file}")
474+
cmake_path(GET yaml_path STEM yaml_file_no_ext)
475+
set(json_file ${yaml_file_no_ext}_${genex_save_count}.json)
476+
set_property(TARGET ${save_target} PROPERTY json_file ${json_file})
477+
478+
# comment this to keep the temporary JSON files
479+
get_property(temp_files TARGET ${save_target} PROPERTY temp_files)
480+
list(APPEND temp_files ${json_file})
481+
set_property(TARGET ${save_target} PROPERTY temp_files ${temp_files})
482+
483+
FILE(GENERATE OUTPUT ${json_file}
484+
CONTENT "${json_content}"
485+
)
486+
endif()
407487
endfunction()
408488

409-
function(to_yaml json level yaml)
489+
function(to_yaml json level yaml genex)
410490
if(level GREATER 0)
411491
math(EXPR level_dec "${level} - 1")
412492
set(indent_${level} "${indent_${level_dec}} ")
@@ -425,10 +505,12 @@ function(to_yaml json level yaml)
425505
string(JSON type TYPE "${json}" ${member})
426506
string(JSON subjson GET "${json}" ${member})
427507
if(type STREQUAL OBJECT)
508+
# JSON object -> YAML dictionary
428509
set(${yaml} "${${yaml}}${indent_${level}}${member}:\n")
429510
math(EXPR sublevel "${level} + 1")
430-
to_yaml("${subjson}" ${sublevel} ${yaml})
511+
to_yaml("${subjson}" ${sublevel} ${yaml} ${genex})
431512
elseif(type STREQUAL ARRAY)
513+
# JSON array -> YAML list
432514
set(${yaml} "${${yaml}}${indent_${level}}${member}:")
433515
string(JSON arraylength LENGTH "${subjson}")
434516
if(${arraylength} LESS 1)
@@ -441,7 +523,33 @@ function(to_yaml json level yaml)
441523
set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n")
442524
endforeach()
443525
endif()
526+
elseif(type STREQUAL STRING)
527+
# JSON string maps to multiple YAML types:
528+
# - with unexpanded generator expressions: save as YAML comment
529+
# - if it matches the special prefix: convert to YAML list
530+
# - otherwise: save as YAML scalar
531+
if (subjson MATCHES "\\$<.*>" AND ${genex})
532+
# Yet unexpanded generator expression: save as comment
533+
string(SUBSTRING ${indent_${level}} 1 -1 short_indent)
534+
set(${yaml} "${${yaml}}#${short_indent}${member}: ${subjson}\n")
535+
elseif(subjson MATCHES "^@YAML-LIST@")
536+
# List-as-string: convert to list
537+
set(${yaml} "${${yaml}}${indent_${level}}${member}:")
538+
list(POP_FRONT subjson)
539+
if(subjson STREQUAL "")
540+
set(${yaml} "${${yaml}} []\n")
541+
else()
542+
set(${yaml} "${${yaml}}\n")
543+
foreach(item ${subjson})
544+
set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n")
545+
endforeach()
546+
endif()
547+
else()
548+
# Raw strings: save as is
549+
set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n")
550+
endif()
444551
else()
552+
# Other JSON data type -> YAML scalar, as-is
445553
set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n")
446554
endif()
447555
endforeach()

cmake/yaml-filter.cmake

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) 2024 Arduino SA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Simple second stage filter for YAML generation, used when generator
5+
# expressions have been used for some of the data and the conversion to
6+
# YAML needs to happen after cmake has completed processing.
7+
#
8+
# This scripts expects as input:
9+
# - JSON_FILE: the name of the input file, in JSON format, that contains
10+
# the expanded generator expressions.
11+
# - YAML_FILE: the name of the final output YAML file.
12+
# - TEMP_FILES: a list of temporary files that need to be removed after
13+
# the conversion is done.
14+
#
15+
# This script loads the Zephyr yaml module and reuses its `to_yaml()`
16+
# function to convert the fully expanded JSON content to YAML, taking
17+
# into account the special format that was used to store lists.
18+
# Temporary files are then removed.
19+
20+
cmake_minimum_required(VERSION 3.20.0)
21+
22+
set(ZEPHYR_BASE ${CMAKE_CURRENT_LIST_DIR}/../)
23+
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules")
24+
include(yaml)
25+
26+
file(READ ${JSON_FILE} json_content)
27+
to_yaml("${json_content}" 0 yaml_out TRUE)
28+
file(WRITE ${YAML_FILE} "${yaml_out}")
29+
30+
# Remove unused temporary files. JSON_FILE needs to be kept, or the
31+
# build system will complain there is no rule to rebuild it
32+
list(REMOVE_ITEM TEMP_FILES ${JSON_FILE})
33+
foreach(file ${TEMP_FILES})
34+
file(REMOVE ${file})
35+
endforeach()

0 commit comments

Comments
 (0)