arch(cmake): add native Kconfig support to cmake (#7934)

This commit is contained in:
David Truan
2025-04-03 14:42:18 +02:00
committed by GitHub
parent e63f345fec
commit 371c21bbda
19 changed files with 1001 additions and 46 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
#
# Generate the cmake variables CONFIG_LV_USE_* from the
# preprocessed lv_conf_internal.h
#
# Author: David TRUAN (david.truan@edgemtech.ch)
#
import os
import argparse
def fatal(msg):
print()
print("ERROR! " + msg)
exit(1)
def get_args():
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=""
"Convert the expanded lv_conf_internal.h to cmake variables."
"It converts all LV_USE_* configurations."
)
parser.add_argument('--input', type=str, required=True, nargs='?',
help='Path of the macro expanded lv_conf_internal.h, which should be generated during a cmake build')
parser.add_argument('--output', type=str, required=True, nargs='?',
help='Path of the output file, where the cmake variables declaration will be written (ex: build/lv_conf.cmake)')
parser.add_argument("--kconfig", action="store_true", help="Enable kconfig flag")
args = parser.parse_args()
# The input must exist
if not os.path.exists(args.input):
fatal(f"Input {args.input} not found")
return args
def generate_cmake_variables(path_input: str, path_output: str, kconfig: bool):
fin = open(path_input)
fout = open(path_output, "w", newline='')
# If we use Kconfig, we must check the CONFIG_LV_USE_* defines
if kconfig:
CONFIG_PATTERN="#define CONFIG_LV_USE"
CONFIG_PREFIX=""
# Otherwise check the LV_USE_* defines
else:
CONFIG_PATTERN="#define LV_USE"
CONFIG_PREFIX="CONFIG_"
# Using the expanded lv_conf_internal, we don't have to deal with regexp,
# as all the #define will be aligned on the left with a single space before the value
for line in fin.read().splitlines():
# Treat the LV_USE_STDLIB_* configs in a special way, as we need
# to convert the define to full config with 1 value when enabled
if line.startswith(f'{CONFIG_PATTERN}_STDLIB'):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
type = value.split("LV_STDLIB_")[1]
name = name.replace("STDLIB", type)
fout.write(f'set({CONFIG_PREFIX}{name} 1)\n')
# Treat the LV_USE_OS config in a special way, as we need
# to convert the define to full config with 1 value when enabled
if line.startswith(f'{CONFIG_PATTERN}_OS'):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
type = value.split("LV_OS")[1]
name += type
fout.write(f'set({CONFIG_PREFIX}{name} 1)\n')
# For the rest of config, simply add CONFIG_ and write
# all LV_USE_* configs, as these are the one needed in cmake
elif line.startswith(f'{CONFIG_PATTERN}'):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
fout.write(f'set({CONFIG_PREFIX}{name} {value})\n')
if __name__ == '__main__':
args = get_args()
generate_cmake_variables(args.input, args.output, args.kconfig)
+1 -1
View File
@@ -1,4 +1,4 @@
vcpkg install vcpkg-tool-ninja libpng freetype opengl glfw3 glew
if %errorlevel% neq 0 exit /b %errorlevel%
pip install pypng lz4 kconfiglib
pip install pypng lz4 kconfiglib pcpp
if %errorlevel% neq 0 exit /b %errorlevel%
+1 -1
View File
@@ -14,4 +14,4 @@ sudo apt install gcc gcc-multilib g++-multilib ninja-build \
ruby-full gcovr cmake python3 libinput-dev libxkbcommon-dev \
libdrm-dev pkg-config wayland-protocols libwayland-dev libwayland-bin \
libwayland-dev:i386 libxkbcommon-dev:i386 libudev-dev
pip3 install pypng lz4 kconfiglib
pip3 install pypng lz4 kconfiglib pcpp
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python3
# Originally modified from:
# https://github.com/zephyrproject-rtos/zephyr/blob/main/scripts/kconfig/kconfig.py
# SPDX-License-Identifier: ISC
# Writes/updates the lvgl/.config configuration file by merging configuration
# files passed as arguments
#
# When fragments haven't changed, lvgl/.config is both the input and the
# output, which just updates it. This is handled in the CMake files.
#
# Also does various checks (most via Kconfiglib warnings).
import argparse
import os
import re
import sys
import textwrap
# Lvgl doesn't use tristate symbols. They're supported here just to make the
# script a bit more generic.
from kconfiglib import (
Kconfig,
split_expr,
expr_value,
expr_str,
BOOL,
TRISTATE,
TRI_TO_STR,
AND,
OR,
)
def main():
print(sys.argv)
args = parse_args()
print("Parsing " + args.kconfig_file)
kconf = Kconfig(args.kconfig_file, warn_to_stderr=False, suppress_traceback=True)
if args.handwritten_input_configs:
# Warn for assignments to undefined symbols, but only for handwritten
# fragments, to avoid warnings-turned-errors when using an old
# configuration file together with updated Kconfig files
kconf.warn_assign_undef = True
# prj.conf may override settings from the board configuration, so
# disable warnings about symbols being assigned more than once
kconf.warn_assign_override = False
kconf.warn_assign_redun = False
if args.forced_input_configs:
# Do not warn on a redundant config.
# The reason is that a regular .config will be followed by the forced
# config which under normal circumstances should be identical to the
# configured setting.
# Only if user has modified to a value that gets overruled by the forced
# a warning shall be issued.
kconf.warn_assign_redun = False
# Load files
print(kconf.load_config(args.configs_in[0]))
for config in args.configs_in[1:]:
# replace=False creates a merged configuration
print(kconf.load_config(config, replace=False))
if args.handwritten_input_configs:
# Check that there are no assignments to promptless symbols, which
# have no effect.
#
# This only makes sense when loading handwritten fragments and not when
# loading lvgl/.config, because lvgl/.config is configuration
# output and also assigns promptless symbols.
check_no_promptless_assign(kconf)
# Print warnings for symbols that didn't get the assigned value. Only
# do this for handwritten input too, to avoid likely unhelpful warnings
# when using an old configuration and updating Kconfig files.
check_assigned_sym_values(kconf)
check_assigned_choice_values(kconf)
if kconf.syms.get("WARN_DEPRECATED", kconf.y).tri_value == 2:
check_deprecated(kconf)
if kconf.syms.get("WARN_EXPERIMENTAL", kconf.y).tri_value == 2:
check_experimental(kconf)
# Hack: Force all symbols to be evaluated, to catch warnings generated
# during evaluation. Wait till the end to write the actual output files, so
# that we don't generate any output if there are warnings-turned-errors.
#
# Kconfiglib caches calculated symbol values internally, so this is still
# fast.
kconf.write_config(os.devnull)
warn_only = r"warning:.*set more than once."
if kconf.warnings:
if args.forced_input_configs:
error_out = False
else:
error_out = True
# Put a blank line between warnings to make them easier to read
for warning in kconf.warnings:
print("\n" + warning, file=sys.stderr)
if not error_out and not re.search(warn_only, warning):
# The warning is not a warn_only, fail the Kconfig.
error_out = True
# Turn all warnings into errors, so that e.g. assignments to undefined
# Kconfig symbols become errors.
#
# A warning is generated by this script whenever a symbol gets a
# different value than the one it was assigned. Keep that one as just a
# warning for now.
if error_out:
err("Aborting due to Kconfig warnings")
# Write the merged configuration and the C header
print(kconf.write_config(args.config_out))
print(kconf.write_autoconf(args.header_out))
# Write the list of parsed Kconfig files to a file
write_kconfig_filenames(kconf, args.kconfig_list_out)
def check_no_promptless_assign(kconf):
# Checks that no promptless symbols are assigned
for sym in kconf.unique_defined_syms:
if sym.user_value is not None and promptless(sym):
err(
f"""\
{sym.name_and_loc} is assigned in a configuration file, but is not directly
user-configurable (has no prompt). It gets its value indirectly from other
symbols. """
+ SYM_INFO_HINT.format(sym)
)
def check_assigned_sym_values(kconf):
# Verifies that the values assigned to symbols "took" (matches the value
# the symbols actually got), printing warnings otherwise. Choice symbols
# are checked separately, in check_assigned_choice_values().
for sym in kconf.unique_defined_syms:
if sym.choice:
continue
user_value = sym.user_value
if user_value is None:
continue
# Tristate values are represented as 0, 1, 2. Having them as "n", "m",
# "y" is more convenient here, so convert.
if sym.type in (BOOL, TRISTATE):
user_value = TRI_TO_STR[user_value]
if user_value != sym.str_value:
msg = (
f"{sym.name_and_loc} was assigned the value '{user_value}'"
f" but got the value '{sym.str_value}'. "
)
# List any unsatisfied 'depends on' dependencies in the warning
mdeps = missing_deps(sym)
if mdeps:
expr_strs = []
for expr in mdeps:
estr = expr_str(expr)
if isinstance(expr, tuple):
# Add () around dependencies that aren't plain symbols.
# Gives '(FOO || BAR) (=n)' instead of
# 'FOO || BAR (=n)', which might be clearer.
estr = f"({estr})"
expr_strs.append(f"{estr} " f"(={TRI_TO_STR[expr_value(expr)]})")
msg += (
"Check these unsatisfied dependencies: "
+ ", ".join(expr_strs)
+ ". "
)
warn(msg + SYM_INFO_HINT.format(sym))
def missing_deps(sym):
# check_assigned_sym_values() helper for finding unsatisfied dependencies.
#
# Given direct dependencies
#
# depends on <expr> && <expr> && ... && <expr>
#
# on 'sym' (which can also come from e.g. a surrounding 'if'), returns a
# list of all <expr>s with a value less than the value 'sym' was assigned
# ("less" instead of "not equal" just to be general and handle tristates,
# even though lvgl doesn't use them).
#
# For string/int/hex symbols, just looks for <expr> = n.
#
# Note that <expr>s can be something more complicated than just a symbol,
# like 'FOO || BAR' or 'FOO = "string"'.
deps = split_expr(sym.direct_dep, AND)
if sym.type in (BOOL, TRISTATE):
return [dep for dep in deps if expr_value(dep) < sym.user_value]
# string/int/hex
return [dep for dep in deps if expr_value(dep) == 0]
def check_assigned_choice_values(kconf):
# Verifies that any choice symbols that were selected (by setting them to
# y) ended up as the selection, printing warnings otherwise.
#
# We check choice symbols separately to avoid warnings when two different
# choice symbols within the same choice are set to y. This might happen if
# a choice selection from a board defconfig is overridden in a prj.conf,
# for example. The last choice symbol set to y becomes the selection (and
# all other choice symbols get the value n).
#
# Without special-casing choices, we'd detect that the first symbol set to
# y ended up as n, and print a spurious warning.
for choice in kconf.unique_choices:
if choice.user_selection and choice.user_selection is not choice.selection:
warn(
f"""\
The choice symbol {choice.user_selection.name_and_loc} was selected (set =y),
but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended
up as the choice selection. """
+ SYM_INFO_HINT.format(choice.user_selection)
)
# Hint on where to find symbol information. Used like
# SYM_INFO_HINT.format(sym).
SYM_INFO_HINT = """\
See https://docs.lvgl.io/master/intro/add-lvgl-to-your-project/configuration.html
look up {0.name} in the menuconfig/guiconfig interface. The Application
Development Primer, Setting Configuration Values, and Kconfig - Tips and Best
Practices sections of the manual might be helpful too.\
"""
def check_deprecated(kconf):
deprecated = kconf.syms.get("DEPRECATED")
dep_expr = kconf.n if deprecated is None else deprecated.rev_dep
if dep_expr is not kconf.n:
selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
for selector in selectors:
selector_name = split_expr(selector, AND)[0].name
warn(f"Deprecated symbol {selector_name} is enabled.")
def check_experimental(kconf):
experimental = kconf.syms.get("EXPERIMENTAL")
dep_expr = kconf.n if experimental is None else experimental.rev_dep
if dep_expr is not kconf.n:
selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
for selector in selectors:
selector_name = split_expr(selector, AND)[0].name
warn(f"Experimental symbol {selector_name} is enabled.")
def promptless(sym):
# Returns True if 'sym' has no prompt. Since the symbol might be defined in
# multiple locations, we need to check all locations.
return not any(node.prompt for node in sym.nodes)
def write_kconfig_filenames(kconf, kconfig_list_path):
# Writes a sorted list with the absolute paths of all parsed Kconfig files
# to 'kconfig_list_path'. The paths are realpath()'d, and duplicates are
# removed. This file is used by CMake to look for changed Kconfig files. It
# needs to be deterministic.
with open(kconfig_list_path, "w") as out:
for path in sorted(
{
os.path.realpath(os.path.join(kconf.srctree, path))
for path in kconf.kconfig_filenames
}
):
print(path, file=out)
def parse_args():
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument(
"--handwritten-input-configs",
action="store_true",
help="Assume the input configuration fragments are "
"handwritten fragments and do additional checks "
"on them, like no promptless symbols being "
"assigned",
)
parser.add_argument(
"--forced-input-configs",
action="store_true",
help="Indicate the input configuration files are "
"followed by an forced configuration file."
"The forced configuration is used to forcefully "
"set specific configuration settings to a "
"pre-defined value and thereby remove any user "
" adjustments.",
)
parser.add_argument("kconfig_file", help="Top-level Kconfig file")
parser.add_argument("config_out", help="Output configuration file")
parser.add_argument("header_out", help="Output header file")
parser.add_argument(
"kconfig_list_out", help="Output file for list of parsed Kconfig files"
)
parser.add_argument(
"configs_in",
nargs="+",
help="Input configuration fragments. Will be merged " "together.",
)
return parser.parse_args()
def warn(msg):
# Use a large fill() width to try to avoid linebreaks in the symbol
# reference link, and add some extra newlines to set the message off from
# surrounding text (this usually gets printed as part of spammy CMake
# output)
print("\n" + textwrap.fill("warning: " + msg, 100) + "\n", file=sys.stderr)
def err(msg):
sys.exit("\n" + textwrap.fill("error: " + msg, 100) + "\n")
if __name__ == "__main__":
main()
+11 -2
View File
@@ -214,12 +214,21 @@ LV_EXPORT_CONST_INT(LV_DRAW_BUF_ALIGN);
#define LV_USE_MEM_MONITOR 0
#endif /*LV_USE_SYSMON*/
#ifndef LV_USE_LZ4
#define LV_USE_LZ4 (LV_USE_LZ4_INTERNAL || LV_USE_LZ4_EXTERNAL)
#if (LV_USE_LZ4_INTERNAL || LV_USE_LZ4_EXTERNAL)
#define LV_USE_LZ4 1
#else
#define LV_USE_LZ4 0
#endif
#endif
#ifndef LV_USE_THORVG
#define LV_USE_THORVG (LV_USE_THORVG_INTERNAL || LV_USE_THORVG_EXTERNAL)
#if (LV_USE_THORVG_INTERNAL || LV_USE_THORVG_EXTERNAL)
#define LV_USE_THORVG 1
#else
#define LV_USE_THORVG 0
#endif
#endif
#if LV_USE_OS
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
#
# Preprocess the lv_conf_internal.h to generate a header file
# containing the evaluated definitions. This output will be used to
# generate the cmake variables
#
# Author: David TRUAN (david.truan@edgemtech.ch)
#
import subprocess
import os
import argparse
import re
def get_args():
parser = argparse.ArgumentParser(description="Preprocess a C header file and remove indentation.")
parser.add_argument("--input", help="Path to the input C header file", required=True)
parser.add_argument("--tmp_file", help="Path to save the preprocessed output", required=True)
parser.add_argument("--output", help="Path to save the cleaned output with removed indentation", required=True)
parser.add_argument(
"--defs",
nargs='+',
default=[],
help="Definitions to be added to pcpp (flag -D)"
)
parser.add_argument(
"--include",
nargs='+',
default=[],
help="Paths to include directories for the preprocessor (flag -I)"
)
return parser.parse_args()
def preprocess_file(input_file, tmp_file, output_file, include_dirs, defs):
try:
pcpp_command = ["pcpp", "-o", tmp_file, "--passthru-defines", "--line-directive=", input_file]
for include_path in include_dirs:
pcpp_command.append(f"-I{include_path}")
for definition in defs:
pcpp_command.append(f"-D{definition}")
subprocess.run(pcpp_command, check=True)
print(f"Preprocessing completed. Output saved to {tmp_file}")
except subprocess.CalledProcessError as e:
print(f"Error during preprocessing: {e}")
exit(1)
def remove_indentation(tmp_file, output_file):
try:
with open(tmp_file, "r") as f:
lines = f.readlines()
clean_lines = []
for line in lines:
stripped = line.lstrip()
# Remove extra spaces after #
if stripped.startswith("#"):
stripped = re.sub(r"^#\s+", "#", stripped)
clean_lines.append(stripped)
with open(output_file, "w") as f:
f.writelines(clean_lines)
print(f"Indentation removed. Cleaned output saved to {output_file}")
os.remove(tmp_file)
print(f"Temporary preprocessed file {tmp_file} removed.")
except Exception as e:
print(f"Error during indentation removal: {e}")
exit(1)
def main():
args = get_args()
preprocess_file(args.input, args.tmp_file, args.output, args.include, args.defs)
remove_indentation(args.tmp_file, args.output)
if __name__ == "__main__":
main()